diff --git a/.cargo/config.toml b/.cargo/config.toml index 65635d8134..4781cd1929 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,6 +4,12 @@ xtask = "run --package xtask --" [build] +# An explicit RUSTFLAGS environment variable replaces these flags, so callers that set it must +# also enable the NuTachyon cfg. +rustflags = [ + '--cfg', + 'zcash_unstable="nutachyon"', +] rustdocflags = [ # Keep shared rustdoc lint levels synchronized with the book and lint # workflows. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 59db3faf69..909e7e9657 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -134,6 +134,10 @@ jobs: # coverage. `zakura-watchdog` is excluded: it depends on `sentry`, which # requires a newer rustc than the workspace library MSRV. - run: cargo check --workspace --bins --exclude zakura-watchdog + env: + # NuTachyon's Tachyon dependency requires Rust 1.97. Clear the repository-level + # opt-in cfg so this job continues to check the supported cfg-off build on 1.91. + RUSTFLAGS: "" docs: name: docs diff --git a/.github/workflows/semver-checks.yml b/.github/workflows/semver-checks.yml index b7c6aab92f..df2b4a63a6 100644 --- a/.github/workflows/semver-checks.yml +++ b/.github/workflows/semver-checks.yml @@ -269,7 +269,7 @@ jobs: run: | # cargo-public-api builds both the current and published crate with # nightly. Nightly-only warnings are evidence, not the lint gate. - RUSTFLAGS='' cargo public-api diff latest -p "$PACKAGE" -sss + RUSTFLAGS='--cfg zcash_unstable="nutachyon"' cargo public-api diff latest -p "$PACKAGE" -sss warm-baselines: name: Warm semver baselines diff --git a/.github/workflows/test-crates.yml b/.github/workflows/test-crates.yml index d7556a3b32..8cc9dff3c0 100644 --- a/.github/workflows/test-crates.yml +++ b/.github/workflows/test-crates.yml @@ -101,8 +101,8 @@ jobs: cargo hack check --workspace cargo hack check --workspace --no-default-features env: - # Treat warnings as errors, preserving the old `features` job behavior. - RUSTFLAGS: -D warnings + # Treat warnings as errors while preserving the workspace NuTachyon cfg. + RUSTFLAGS: '--cfg zcash_unstable="nutachyon" -D warnings' - name: Check publishable crate packaging run: ./scripts/check-crate-packaging.sh diff --git a/Cargo.lock b/Cargo.lock index a9b1e5a727..c76f2b2c91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1669,8 +1669,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/zcash/librustzcash.git?rev=c7235b1fc4b4296b8af2e26bf2f6adb24f873f56#c7235b1fc4b4296b8af2e26bf2f6adb24f873f56" dependencies = [ "blake2b_simd", ] @@ -3369,6 +3368,15 @@ dependencies = [ "rayon", ] +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "git+https://github.com/tachyon-zcash/maybe-rayon.git?rev=5c37ee0dd448e8c1edc4529d0b8897ecaff5aad1#5c37ee0dd448e8c1edc4529d0b8897ecaff5aad1" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "md5" version = "0.7.0" @@ -4716,6 +4724,100 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +[[package]] +name = "ragu" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/ragu?rev=f4f6315b10ed96944c40b2bbdbca0fe06af66ce0#f4f6315b10ed96944c40b2bbdbca0fe06af66ce0" +dependencies = [ + "blake2b_simd", + "ragu_arithmetic", + "ragu_circuits", + "ragu_core", + "ragu_pasta", + "ragu_pcd", + "ragu_primitives", +] + +[[package]] +name = "ragu_arithmetic" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/ragu?rev=f4f6315b10ed96944c40b2bbdbca0fe06af66ce0#f4f6315b10ed96944c40b2bbdbca0fe06af66ce0" +dependencies = [ + "ff", + "group", + "maybe-rayon 0.1.1 (git+https://github.com/tachyon-zcash/maybe-rayon.git?rev=5c37ee0dd448e8c1edc4529d0b8897ecaff5aad1)", + "ragu_macros", + "rand 0.10.2", + "zakura-pasta-curves", +] + +[[package]] +name = "ragu_circuits" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/ragu?rev=f4f6315b10ed96944c40b2bbdbca0fe06af66ce0#f4f6315b10ed96944c40b2bbdbca0fe06af66ce0" +dependencies = [ + "blake2b_simd", + "maybe-rayon 0.1.1 (git+https://github.com/tachyon-zcash/maybe-rayon.git?rev=5c37ee0dd448e8c1edc4529d0b8897ecaff5aad1)", + "ragu_arithmetic", + "ragu_core", + "ragu_primitives", +] + +[[package]] +name = "ragu_core" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/ragu?rev=f4f6315b10ed96944c40b2bbdbca0fe06af66ce0#f4f6315b10ed96944c40b2bbdbca0fe06af66ce0" +dependencies = [ + "ragu_arithmetic", + "ragu_macros", + "thiserror 2.0.18", +] + +[[package]] +name = "ragu_macros" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/ragu?rev=f4f6315b10ed96944c40b2bbdbca0fe06af66ce0#f4f6315b10ed96944c40b2bbdbca0fe06af66ce0" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ragu_pasta" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/ragu?rev=f4f6315b10ed96944c40b2bbdbca0fe06af66ce0#f4f6315b10ed96944c40b2bbdbca0fe06af66ce0" +dependencies = [ + "lazy_static", + "ragu_arithmetic", +] + +[[package]] +name = "ragu_pcd" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/ragu?rev=f4f6315b10ed96944c40b2bbdbca0fe06af66ce0#f4f6315b10ed96944c40b2bbdbca0fe06af66ce0" +dependencies = [ + "maybe-rayon 0.1.1 (git+https://github.com/tachyon-zcash/maybe-rayon.git?rev=5c37ee0dd448e8c1edc4529d0b8897ecaff5aad1)", + "ragu_arithmetic", + "ragu_circuits", + "ragu_core", + "ragu_primitives", +] + +[[package]] +name = "ragu_primitives" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/ragu?rev=f4f6315b10ed96944c40b2bbdbca0fe06af66ce0#f4f6315b10ed96944c40b2bbdbca0fe06af66ce0" +dependencies = [ + "ragu_arithmetic", + "ragu_core", + "ragu_macros", + "thiserror 2.0.18", +] + [[package]] name = "rand" version = "0.8.6" @@ -7697,8 +7799,7 @@ checksum = "cdad84f915d23f50eeb7389fd1ffc2385b35c57c68ceb2df20c73b7724621a30" [[package]] name = "zakura-bellman" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a6b7239d41621cc0c39349882001442d49bbc8dc46390c4826feb85a55a8d47" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "bitvec", "blake2s_simd", @@ -7714,8 +7815,7 @@ dependencies = [ [[package]] name = "zakura-bls12-381" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2c114620056f8c97ecc17d7333add6c818ff376434943c7ff4974320426887" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "ff", "group", @@ -7754,6 +7854,7 @@ dependencies = [ "primitive-types", "proptest", "proptest-derive", + "ragu", "rand 0.10.2", "rand 0.8.6", "rand_chacha 0.10.0", @@ -7777,6 +7878,7 @@ dependencies = [ "uint 0.10.0", "x25519-dalek", "zakura-halo2-proofs", + "zakura-history", "zakura-jubjub", "zakura-orchard", "zakura-primitives", @@ -7786,10 +7888,10 @@ dependencies = [ "zakura-sinsemilla", "zakura-test", "zcash_address", - "zcash_history", "zcash_note_encryption", "zcash_protocol", "zcash_script", + "zcash_tachyon", "zcash_transparent", ] @@ -7812,6 +7914,7 @@ dependencies = [ "once_cell", "proptest", "proptest-derive", + "ragu", "rand 0.10.2", "rand 0.8.6", "rayon", @@ -7839,14 +7942,14 @@ dependencies = [ "zakura-tower-fallback", "zcash_protocol", "zcash_script", + "zcash_tachyon", "zcash_transparent", ] [[package]] name = "zakura-halo2-gadgets" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af79f971003d67f4a198985469834c083f37354d604bf93f892c9c9f5a129ae0" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "arrayvec", "ff", @@ -7861,14 +7964,12 @@ dependencies = [ [[package]] name = "zakura-halo2-legacy-pdqsort" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c91d2f5fd4fc95a9c42d43744fa9512b2d7d996ea454f3b21faac6fb36a9806" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" [[package]] name = "zakura-halo2-poseidon" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdac6fc73528451edc4ee8cff55946ff2c75359c47937eb42e6d31b83b542d39" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "bitvec", "ff", @@ -7879,13 +7980,12 @@ dependencies = [ [[package]] name = "zakura-halo2-proofs" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5875a590a7d9175fdb47286ca0e5073a1e9ee78775b70ca4309353cf19a6b6ed" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "blake2b_simd", "ff", "group", - "maybe-rayon", + "maybe-rayon 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.10.2", "rand_core 0.10.1", "tracing", @@ -7908,6 +8008,17 @@ dependencies = [ "zakura-test", ] +[[package]] +name = "zakura-history" +version = "1.0.0" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" +dependencies = [ + "blake2b_simd", + "corez", + "primitive-types", + "zcash_encoding 0.5.0", +] + [[package]] name = "zakura-jsonl-trace" version = "1.2.0" @@ -7923,8 +8034,7 @@ dependencies = [ [[package]] name = "zakura-jubjub" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9915c2c7f2c021379453608c91548f2811d1ba59b7e8505bb8ba32f51e494db" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "bitvec", "ff", @@ -7937,8 +8047,7 @@ dependencies = [ [[package]] name = "zakura-keys" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df47ed252741a6fcefc2b92aa773034c602eca361c5f974ffbb4f67a49fe2c78" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "bech32", "blake2b_simd", @@ -7953,7 +8062,7 @@ dependencies = [ "zakura-orchard", "zakura-sapling-crypto", "zcash_address", - "zcash_encoding", + "zcash_encoding 0.4.0", "zcash_protocol", "zcash_transparent", "zip32", @@ -8022,8 +8131,7 @@ dependencies = [ [[package]] name = "zakura-orchard" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7edaa754f9bafb4965559e38af6d337eb001f76de666fe20e510fb8e1cf631" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "aes", "bitvec", @@ -8035,7 +8143,7 @@ dependencies = [ "group", "hex", "incrementalmerkletree", - "maybe-rayon", + "maybe-rayon 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "memuse", "nonempty", "once_cell", @@ -8059,8 +8167,7 @@ dependencies = [ [[package]] name = "zakura-pairing" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59445f1cb78bef29da023f8ecdba607ed00f2df51c245e1d50964af2a3e7b800" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "group", ] @@ -8068,14 +8175,13 @@ dependencies = [ [[package]] name = "zakura-pasta-curves" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e878bab2110a70ed4496e377b4b3f3039d6a227c5d79e171b316f334857531b9" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "blake2b_simd", "cc", "ff", "group", - "maybe-rayon", + "maybe-rayon 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "once_cell", "rand 0.10.2", "static_assertions", @@ -8085,8 +8191,7 @@ dependencies = [ [[package]] name = "zakura-primitives" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799de508ecd41846dd0dfd06490e30ec6ee918008ceba19155e09f2545f3df73" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "blake2b_simd", "block-buffer 0.11.0-rc.3", @@ -8106,18 +8211,18 @@ dependencies = [ "zakura-orchard", "zakura-redjubjub", "zakura-sapling-crypto", - "zcash_encoding", + "zcash_encoding 0.4.0", "zcash_note_encryption", "zcash_protocol", "zcash_script", + "zcash_tachyon", "zcash_transparent", ] [[package]] name = "zakura-proofs" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "506fc870df35b86053fa50a24f3f02c50189a3a609c62478bbd1b7c94943d188" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "blake2b_simd", "document-features", @@ -8136,8 +8241,7 @@ dependencies = [ [[package]] name = "zakura-reddsa" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3391ff6c3e6222f5176c046e1d3645748be5b211880747e279d0f17860d51526" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "blake2b_simd", "frost-rerandomized", @@ -8154,8 +8258,7 @@ dependencies = [ [[package]] name = "zakura-redjubjub" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "174ffd325c22f5dbb4c4dfe6f825bd82c3ca88a91bbe603b5cdf0dd7590d0a9c" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "rand_core 0.10.1", "serde", @@ -8170,6 +8273,7 @@ version = "9.0.0" dependencies = [ "anyhow", "base64 0.22.1", + "blake2b_simd", "bytes", "chrono", "color-eyre", @@ -8193,6 +8297,7 @@ dependencies = [ "phf", "proptest", "prost", + "ragu", "rand 0.10.2", "rand 0.8.6", "reqwest 0.12.28", @@ -8230,14 +8335,14 @@ dependencies = [ "zcash_address", "zcash_protocol", "zcash_script", + "zcash_tachyon", "zcash_transparent", ] [[package]] name = "zakura-sapling-crypto" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadf81594078e0e18f6c1744d978a9ee418fce988c293567131bc4870247ee85" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "aes", "bitvec", @@ -8288,8 +8393,7 @@ dependencies = [ [[package]] name = "zakura-sinsemilla" version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "582ec66fe58b717034b8f5c8b54834d93abd515fc20a0ce2ab769d522432b8ca" +source = "git+https://github.com/zakura-core/common.git?rev=88473230387865b2b5c6770c0cfbcd943f56cbce#88473230387865b2b5c6770c0cfbcd943f56cbce" dependencies = [ "group", "once_cell", @@ -8345,6 +8449,7 @@ dependencies = [ "zakura-orchard", "zakura-sapling-crypto", "zakura-test", + "zcash_tachyon", ] [[package]] @@ -8445,14 +8550,13 @@ dependencies = [ [[package]] name = "zcash_address" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a854b28c07dba372f4410ea8ad62b4bf7d5c2bf8be32fc4b31bc0db6521a975" +source = "git+https://github.com/zcash/librustzcash.git?rev=c7235b1fc4b4296b8af2e26bf2f6adb24f873f56#c7235b1fc4b4296b8af2e26bf2f6adb24f873f56" dependencies = [ "bech32", "bs58", "corez", "f4jumble", - "zcash_encoding", + "zcash_encoding 0.4.0", "zcash_protocol", ] @@ -8468,14 +8572,14 @@ dependencies = [ ] [[package]] -name = "zcash_history" +name = "zcash_encoding" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d2ed9a9fa7b466a7adedab1ad5a21f8330e4943756912fc776cf7f3beae32b" +checksum = "b2871fd3c37eaeb635c815a9c97495553bf8237a481b9f8b37eba661764bf937" dependencies = [ - "blake2b_simd", - "byteorder", - "primitive-types", + "corez", + "hex", + "nonempty", ] [[package]] @@ -8494,14 +8598,13 @@ dependencies = [ [[package]] name = "zcash_protocol" version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "314329b91ec4bbb517441840e47d0b2029bf0b946f086980c96c889c2d92dc5d" +source = "git+https://github.com/zcash/librustzcash.git?rev=c7235b1fc4b4296b8af2e26bf2f6adb24f873f56#c7235b1fc4b4296b8af2e26bf2f6adb24f873f56" dependencies = [ "corez", "document-features", "hex", "memuse", - "zcash_encoding", + "zcash_encoding 0.4.0", ] [[package]] @@ -8530,11 +8633,27 @@ dependencies = [ "blake2b_simd", ] +[[package]] +name = "zcash_tachyon" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/tachyon?rev=2dc7010c3e7771c539fd1c03a06bc6be1a7316c7#2dc7010c3e7771c539fd1c03a06bc6be1a7316c7" +dependencies = [ + "blake2b_simd", + "corez", + "derive_more 2.1.1", + "ff", + "group", + "lazy_static", + "ragu", + "rand_core 0.10.1", + "zakura-pasta-curves", + "zakura-reddsa", +] + [[package]] name = "zcash_transparent" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "547c012778bae17f58007731af074d638aa146ab0ecfc120adebf23d049aff6c" +source = "git+https://github.com/zcash/librustzcash.git?rev=c7235b1fc4b4296b8af2e26bf2f6adb24f873f56#c7235b1fc4b4296b8af2e26bf2f6adb24f873f56" dependencies = [ "bip32", "bs58", @@ -8548,7 +8667,7 @@ dependencies = [ "sha2 0.10.9", "subtle", "zcash_address", - "zcash_encoding", + "zcash_encoding 0.4.0", "zcash_protocol", "zcash_script", "zcash_spec", diff --git a/Cargo.toml b/Cargo.toml index 79a50cd20c..c97f32b4dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,26 +49,28 @@ edition = "2021" # gate refers to `zakura-assets` and nothing else. zakura-assets = { version = "=0.3466571.0" } incrementalmerkletree = { version = "0.8.2", features = ["legacy-api"] } -orchard = { version = "1.1.0", package = "zakura-orchard" } -sapling-crypto = { version = "1.1.0", package = "zakura-sapling-crypto" } -zcash_address = "0.13.0" -zcash_history = "0.5.0" -zcash_keys = { version = "1.1.0", package = "zakura-keys" } -zcash_primitives = { version = "1.1.0", package = "zakura-primitives" } -zcash_proofs = { version = "1.1.0", package = "zakura-proofs" } -zcash_transparent = "0.10.0" -zcash_protocol = "0.10.1" +orchard = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-orchard" } +sapling-crypto = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-sapling-crypto" } +zcash_address = { git = "https://github.com/zcash/librustzcash.git", rev = "c7235b1fc4b4296b8af2e26bf2f6adb24f873f56" } +zcash_history = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-history" } +zcash_keys = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-keys" } +zcash_primitives = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-primitives" } +zcash_proofs = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-proofs" } +zcash_transparent = { git = "https://github.com/zcash/librustzcash.git", rev = "c7235b1fc4b4296b8af2e26bf2f6adb24f873f56" } +zcash_protocol = { git = "https://github.com/zcash/librustzcash.git", rev = "c7235b1fc4b4296b8af2e26bf2f6adb24f873f56" } +zcash_tachyon = { git = "https://github.com/tachyon-zcash/tachyon", rev = "2dc7010c3e7771c539fd1c03a06bc6be1a7316c7", version = "0.0.0", default-features = false } +ragu = { git = "https://github.com/tachyon-zcash/ragu", rev = "f4f6315b10ed96944c40b2bbdbca0fe06af66ce0", default-features = false, features = ["mock"] } abscissa_core = { version = "0.7", default-features = false } base64 = "0.22.1" bech32 = "0.11.0" -bellman = { version = "1.1.0", package = "zakura-bellman" } +bellman = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-bellman" } bincode = "1.3" bitflags = "2.9" bitflags-serde-legacy = "0.1.1" bitvec = "1.0" blake2b_simd = "1.0" blake2s_simd = "1.0" -bls12_381 = { version = "1.1.0", package = "zakura-bls12-381" } +bls12_381 = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-bls12-381" } bs58 = "0.5" byteorder = "1.5" bytes = "1.10" @@ -87,7 +89,7 @@ futures = "0.3.31" futures-core = "0.3.31" futures-util = "0.3.31" group = "0.14" -halo2 = { version = "1.1.0", package = "zakura-halo2-proofs" } +halo2 = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-halo2-proofs" } hex = "0.4.3" hex-literal = "0.4" howudoin = "0.1" @@ -106,7 +108,7 @@ itertools = "0.14" jsonrpsee = "0.24.8" jsonrpsee-proc-macros = "0.24.9" jsonrpsee-types = "0.24.9" -jubjub = { version = "1.1.0", package = "zakura-jubjub" } +jubjub = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-jubjub" } lazy_static = "1.4" libzcash_script = "0.1" log = "0.4.27" @@ -129,8 +131,8 @@ rand_10 = { package = "rand", version = "0.10" } rand_chacha_10 = { package = "rand_chacha", version = "0.10" } rand_core_10 = { package = "rand_core", version = "0.10" } rayon = "1.10" -reddsa = { version = "1.1.0", package = "zakura-reddsa" } -redjubjub = { version = "1.1.0", package = "zakura-redjubjub" } +reddsa = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-reddsa" } +redjubjub = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-redjubjub" } regex = "1.11" reqwest = { version = "0.12", default-features = false } ripemd = "0.1" @@ -145,7 +147,7 @@ serde_json = "1.0.140" serde_with = "3.12" sha2 = "0.10" schemars = "1" -sinsemilla = { version = "1.1.0", package = "zakura-sinsemilla" } +sinsemilla = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce", package = "zakura-sinsemilla" } spandoc = "0.2" anyhow = "1.0" strum = "0.27" @@ -183,6 +185,10 @@ zcash_note_encryption = "0.4.1" zcash_script = "0.4.5" config = { version = "0.15.14", default-features = false, features = ["toml"] } +[patch.crates-io] +zakura-pasta-curves = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce" } +zakura-reddsa = { git = "https://github.com/zakura-core/common.git", rev = "88473230387865b2b5c6770c0cfbcd943f56cbce" } + # All cargo-release configuration lives in this table. Do not add a root # release.toml: it would silently shadow every setting here (cargo-release # reads a workspace release.toml in preference to this metadata table). @@ -380,7 +386,7 @@ lto = "off" # The linter should ignore these expected config flags/values unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(tokio_unstable)', # Used by tokio-console - 'cfg(zcash_unstable, values("zfuture", "nu6.1", "nu7"))', # Used in Zakura and librustzcash + 'cfg(zcash_unstable, values("zfuture", "nu6.1", "nu7", "nutachyon"))', # Used in Zakura and librustzcash ] } # High-risk code diff --git a/crates/zakura-chain/Cargo.toml b/crates/zakura-chain/Cargo.toml index 97fd8c83e3..52d0df6cd6 100644 --- a/crates/zakura-chain/Cargo.toml +++ b/crates/zakura-chain/Cargo.toml @@ -129,6 +129,9 @@ proptest-derive = { workspace = true, optional = true } zakura-test = { path = "../zakura-test/", version = "2.1.0", optional = true } bounded-vec = { version = "0.9.0", features = ["serde"] } +[target.'cfg(zcash_unstable = "nutachyon")'.dependencies] +zcash_tachyon = { workspace = true } + [dev-dependencies] # Benchmarks criterion = { workspace = true, features = ["html_reports"] } @@ -150,6 +153,9 @@ tokio = { workspace = true, features = ["full", "tracing", "test-util"] } zakura-test = { path = "../zakura-test/", version = "2.1.0" } +[target.'cfg(zcash_unstable = "nutachyon")'.dev-dependencies] +ragu = { workspace = true } + [[bench]] name = "block" harness = false diff --git a/crates/zakura-chain/src/block.rs b/crates/zakura-chain/src/block.rs index 56b0026d66..9a79d2d479 100644 --- a/crates/zakura-chain/src/block.rs +++ b/crates/zakura-chain/src/block.rs @@ -269,6 +269,18 @@ impl Block { .expect("number of transactions must fit u64") } + /// Count how many Tachyon transactions exist in a block, + /// i.e. transactions carrying a tachyon bundle (NuTachyon). + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_transactions_count(&self) -> u64 { + self.transactions + .iter() + .filter(|tx| tx.has_tachyon_shielded_data()) + .count() + .try_into() + .expect("number of transactions must fit u64") + } + /// Returns the overall chain value pool change in this block---the negative sum of the /// transaction value balances in this block. /// diff --git a/crates/zakura-chain/src/block/arbitrary.rs b/crates/zakura-chain/src/block/arbitrary.rs index 9a29304b42..42ab0bc0ba 100644 --- a/crates/zakura-chain/src/block/arbitrary.rs +++ b/crates/zakura-chain/src/block/arbitrary.rs @@ -430,6 +430,8 @@ impl Block { let mut sapling_tree = sapling::tree::NoteCommitmentTree::default(); let mut orchard_tree = orchard::tree::NoteCommitmentTree::default(); let mut ironwood_tree = ironwood::tree::NoteCommitmentTree::default(); + #[cfg(zcash_unstable = "nutachyon")] + let mut tachyon_anchor = crate::tachyon::Anchor::default(); // The history tree usually takes care of "creating itself". But this // only works when blocks are pushed into it starting from genesis // (or at least pre-Heartwood, where the tree is not required). @@ -481,6 +483,16 @@ impl Block { // delete invalid transactions block.transactions = new_transactions; + #[cfg(zcash_unstable = "nutachyon")] + if let Some(pool_height) = + crate::tachyon::pool_height(¤t.network, block.coinbase_height().unwrap()) + { + tachyon_anchor = tachyon_anchor + .advance_with_block(pool_height, block) + .expect("generated Tachyon stamps have valid anchor transitions") + .post_block; + } + // fix commitment (must be done after finishing changing the block) if generate_valid_commitments { let current_height = block.coinbase_height().unwrap(); @@ -537,6 +549,8 @@ impl Block { &sapling_tree.root(), &orchard_tree.root(), &ironwood_tree.root(), + #[cfg(zcash_unstable = "nutachyon")] + &tachyon_anchor, ) .unwrap(); } else { @@ -547,6 +561,8 @@ impl Block { &sapling_tree.root(), &orchard_tree.root(), &ironwood_tree.root(), + #[cfg(zcash_unstable = "nutachyon")] + &tachyon_anchor, ) .unwrap(), ); @@ -609,6 +625,11 @@ where sapling_shielded_data, .. } => *sapling_shielded_data = None, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data, + .. + } => *sapling_shielded_data = None, Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {} } } diff --git a/crates/zakura-chain/src/block/commitment.rs b/crates/zakura-chain/src/block/commitment.rs index 4141e6d37e..77560a84a5 100644 --- a/crates/zakura-chain/src/block/commitment.rs +++ b/crates/zakura-chain/src/block/commitment.rs @@ -142,6 +142,10 @@ impl Commitment { (Nu5 | Nu6 | Nu6_1 | Nu6_2 | Nu6_3 | Nu7, _) => Ok(ChainHistoryBlockTxAuthCommitment( ChainHistoryBlockTxAuthCommitmentHash(bytes), )), + #[cfg(zcash_unstable = "nutachyon")] + (NuTachyon, _) => Ok(ChainHistoryBlockTxAuthCommitment( + ChainHistoryBlockTxAuthCommitmentHash(bytes), + )), #[cfg(zcash_unstable = "zfuture")] (ZFuture, _) => Ok(ChainHistoryBlockTxAuthCommitment( diff --git a/crates/zakura-chain/src/history_tree.rs b/crates/zakura-chain/src/history_tree.rs index 71ab524507..79f1e18ca2 100644 --- a/crates/zakura-chain/src/history_tree.rs +++ b/crates/zakura-chain/src/history_tree.rs @@ -24,6 +24,8 @@ use crate::{ }, sapling, }; +#[cfg(zcash_unstable = "nutachyon")] +use crate::{primitives::zcash_history::V4 as TachyonOnward, tachyon}; /// An error describing why a history tree operation failed. #[derive(Debug, Error)] @@ -60,6 +62,9 @@ enum InnerHistoryTree { OrchardOnward(Tree), /// An Ironwood-onward tree. IronwoodOnward(Tree), + /// A Tachyon-onward tree. + #[cfg(zcash_unstable = "nutachyon")] + TachyonOnward(Tree), } /// History tree (Merkle mountain range) structure that contains information about @@ -142,6 +147,17 @@ impl NonEmptyHistoryTree { )?; InnerHistoryTree::IronwoodOnward(tree) } + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => { + let tree = Tree::::new_from_cache( + network, + network_upgrade, + size, + &peaks, + &Default::default(), + )?; + InnerHistoryTree::TachyonOnward(tree) + } #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => { @@ -179,10 +195,18 @@ impl NonEmptyHistoryTree { sapling_root: &sapling::tree::Root, orchard_root: &orchard::tree::Root, ironwood_root: &ironwood::tree::Root, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: &tachyon::Anchor, ) -> Result { Self::from_parts( network, - HistoryTreeBlockParts::from_block(&block, sapling_root, orchard_root, ironwood_root), + HistoryTreeBlockParts::from_block( + &block, + sapling_root, + orchard_root, + ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, + ), ) } @@ -230,6 +254,11 @@ impl NonEmptyHistoryTree { let (tree, entry) = Tree::::new_from_parts(network, parts)?; (InnerHistoryTree::IronwoodOnward(tree), entry) } + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => { + let (tree, entry) = Tree::::new_from_parts(network, parts)?; + (InnerHistoryTree::TachyonOnward(tree), entry) + } #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => { @@ -267,12 +296,15 @@ impl NonEmptyHistoryTree { sapling_root: &sapling::tree::Root, orchard_root: &orchard::tree::Root, ironwood_root: &ironwood::tree::Root, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: &tachyon::Anchor, ) -> Result<(), HistoryTreeError> { self.push_from_parts(HistoryTreeBlockParts::from_block( &block, sapling_root, orchard_root, ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, )) } @@ -326,6 +358,10 @@ impl NonEmptyHistoryTree { InnerHistoryTree::IronwoodOnward(tree) => tree .append_leaf_parts(parts) .map_err(|e| HistoryTreeError::InnerError { inner: e })?, + #[cfg(zcash_unstable = "nutachyon")] + InnerHistoryTree::TachyonOnward(tree) => tree + .append_leaf_parts(parts) + .map_err(|e| HistoryTreeError::InnerError { inner: e })?, }; for entry in new_entries { // Not every entry is a peak; those will be trimmed later @@ -338,6 +374,36 @@ impl NonEmptyHistoryTree { } /// Extend the history tree with the given blocks. + #[cfg(zcash_unstable = "nutachyon")] + pub fn try_extend< + 'a, + T: IntoIterator< + Item = ( + Arc, + &'a sapling::tree::Root, + &'a orchard::tree::Root, + &'a ironwood::tree::Root, + &'a tachyon::Anchor, + ), + >, + >( + &mut self, + iter: T, + ) -> Result<(), HistoryTreeError> { + for (block, sapling_root, orchard_root, ironwood_root, tachyon_anchor) in iter { + self.push( + block, + sapling_root, + orchard_root, + ironwood_root, + tachyon_anchor, + )?; + } + Ok(()) + } + + /// Extend the history tree with the given blocks. + #[cfg(not(zcash_unstable = "nutachyon"))] pub fn try_extend< 'a, T: IntoIterator< @@ -460,6 +526,16 @@ impl NonEmptyHistoryTree { &Default::default(), )?) } + #[cfg(zcash_unstable = "nutachyon")] + InnerHistoryTree::TachyonOnward(_) => { + InnerHistoryTree::TachyonOnward(Tree::::new_from_cache( + &self.network, + self.network_upgrade, + self.size, + &self.peaks, + &Default::default(), + )?) + } }; Ok(()) } @@ -470,6 +546,8 @@ impl NonEmptyHistoryTree { InnerHistoryTree::PreOrchard(tree) => tree.hash(), InnerHistoryTree::OrchardOnward(tree) => tree.hash(), InnerHistoryTree::IronwoodOnward(tree) => tree.hash(), + #[cfg(zcash_unstable = "nutachyon")] + InnerHistoryTree::TachyonOnward(tree) => tree.hash(), } } @@ -527,6 +605,17 @@ impl Clone for NonEmptyHistoryTree { ) .expect("rebuilding an existing tree should always work"), ), + #[cfg(zcash_unstable = "nutachyon")] + InnerHistoryTree::TachyonOnward(_) => InnerHistoryTree::TachyonOnward( + Tree::::new_from_cache( + &self.network, + self.network_upgrade, + self.size, + &self.peaks, + &Default::default(), + ) + .expect("rebuilding an existing tree should always work"), + ), }; NonEmptyHistoryTree { network: self.network.clone(), @@ -555,10 +644,18 @@ impl HistoryTree { sapling_root: &sapling::tree::Root, orchard_root: &orchard::tree::Root, ironwood_root: &ironwood::tree::Root, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: &tachyon::Anchor, ) -> Result { Self::from_parts( network, - HistoryTreeBlockParts::from_block(&block, sapling_root, orchard_root, ironwood_root), + HistoryTreeBlockParts::from_block( + &block, + sapling_root, + orchard_root, + ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, + ), ) } @@ -593,10 +690,18 @@ impl HistoryTree { sapling_root: &sapling::tree::Root, orchard_root: &orchard::tree::Root, ironwood_root: &ironwood::tree::Root, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: &tachyon::Anchor, ) -> Result<(), HistoryTreeError> { self.push_from_parts( network, - HistoryTreeBlockParts::from_block(&block, sapling_root, orchard_root, ironwood_root), + HistoryTreeBlockParts::from_block( + &block, + sapling_root, + orchard_root, + ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, + ), ) } diff --git a/crates/zakura-chain/src/history_tree/tests/vectors.rs b/crates/zakura-chain/src/history_tree/tests/vectors.rs index 09ca1c8e5a..b1b9d09e8c 100644 --- a/crates/zakura-chain/src/history_tree/tests/vectors.rs +++ b/crates/zakura-chain/src/history_tree/tests/vectors.rs @@ -16,6 +16,9 @@ use crate::{ serialization::ZcashDeserializeInto, }; +#[cfg(zcash_unstable = "nutachyon")] +const DEFAULT_TACHYON_ANCHOR: crate::tachyon::Anchor = crate::tachyon::Anchor([0; 32]); + use color_eyre::eyre; use eyre::Result; @@ -69,6 +72,8 @@ fn push_and_prune_for_network_upgrade( &first_sapling_root, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; assert_eq!(tree.size(), 1); @@ -102,6 +107,8 @@ fn push_and_prune_for_network_upgrade( &second_sapling_root, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .unwrap(); @@ -176,6 +183,8 @@ fn parts_api_matches_block_api_for_network(network: Network) -> Result<()> { &Default::default(), &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; parts_tree.push_from_parts( &network, @@ -184,6 +193,8 @@ fn parts_api_matches_block_api_for_network(network: Network) -> Result<()> { &Default::default(), &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ), )?; @@ -196,6 +207,8 @@ fn parts_api_matches_block_api_for_network(network: Network) -> Result<()> { &first_sapling_root, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; parts_tree.push_from_parts( &network, @@ -204,6 +217,8 @@ fn parts_api_matches_block_api_for_network(network: Network) -> Result<()> { &first_sapling_root, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ), )?; @@ -226,6 +241,8 @@ fn parts_api_matches_block_api_for_network(network: Network) -> Result<()> { &second_sapling_root, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; parts_tree.push_from_parts( &network, @@ -234,6 +251,8 @@ fn parts_api_matches_block_api_for_network(network: Network) -> Result<()> { &second_sapling_root, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ), )?; @@ -278,9 +297,13 @@ fn from_cache_preserves_ironwood_history_version() -> Result<()> { sapling_root: &Default::default(), orchard_root: &Default::default(), ironwood_root: &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: &DEFAULT_TACHYON_ANCHOR, sapling_tx: 0, orchard_tx: 0, ironwood_tx: 1, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tx: 0, }, )?; // Decoding the same peaks as V2 succeeds, because the serialized V3 @@ -361,6 +384,8 @@ fn from_block_delegation_preserves_all_history_versions() -> Result<()> { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; let parts_tree = NonEmptyHistoryTree::from_parts( &network, @@ -370,9 +395,13 @@ fn from_block_delegation_preserves_all_history_versions() -> Result<()> { sapling_root: &sapling_root, orchard_root: &orchard_root, ironwood_root: &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: &DEFAULT_TACHYON_ANCHOR, sapling_tx: block.sapling_transactions_count(), orchard_tx: block.orchard_transactions_count(), ironwood_tx: block.ironwood_transactions_count(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tx: block.tachyon_transactions_count(), }, )?; @@ -431,9 +460,13 @@ fn from_parts_selects_epoch_version_and_ignores_future_fields() -> Result<()> { sapling_root: &sapling_root, orchard_root, ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: &DEFAULT_TACHYON_ANCHOR, sapling_tx: 1, orchard_tx, ironwood_tx, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tx: 0, }; assert_eq!( @@ -551,6 +584,8 @@ fn push_wrapper_matches_parts_across_nu6_3_activation() -> Result<()> { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; let mut parts_tree = NonEmptyHistoryTree::from_parts( &network, @@ -559,6 +594,8 @@ fn push_wrapper_matches_parts_across_nu6_3_activation() -> Result<()> { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ), )?; @@ -567,12 +604,16 @@ fn push_wrapper_matches_parts_across_nu6_3_activation() -> Result<()> { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; parts_tree.push_from_parts(HistoryTreeBlockParts::from_block( &activation_block, &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ))?; let activation_tree = NonEmptyHistoryTree::from_block( &network, @@ -580,6 +621,8 @@ fn push_wrapper_matches_parts_across_nu6_3_activation() -> Result<()> { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; let activation_without_ironwood = NonEmptyHistoryTree::from_block( &network, @@ -587,6 +630,8 @@ fn push_wrapper_matches_parts_across_nu6_3_activation() -> Result<()> { &sapling_root, &orchard_root, &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; assert_eq!(block_tree.hash(), parts_tree.hash()); @@ -664,9 +709,13 @@ fn push_from_parts_routes_each_history_version() -> Result<()> { sapling_root, orchard_root, ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: &DEFAULT_TACHYON_ANCHOR, sapling_tx: 1, orchard_tx, ironwood_tx, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tx: 0, } } let two_leaf_tree = |network: &Network, @@ -765,18 +814,24 @@ fn try_extend_forwards_ironwood_roots_in_order() -> Result<()> { &sapling_root, &orchard_root, &first_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; sequential.push( second_block.clone(), &sapling_root, &orchard_root, &second_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; sequential.push( third_block.clone(), &sapling_root, &orchard_root, &third_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; let mut extended = NonEmptyHistoryTree::from_block( @@ -785,7 +840,27 @@ fn try_extend_forwards_ironwood_roots_in_order() -> Result<()> { &sapling_root, &orchard_root, &first_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; + #[cfg(zcash_unstable = "nutachyon")] + extended.try_extend([ + ( + second_block.clone(), + &sapling_root, + &orchard_root, + &second_ironwood_root, + &DEFAULT_TACHYON_ANCHOR, + ), + ( + third_block.clone(), + &sapling_root, + &orchard_root, + &third_ironwood_root, + &DEFAULT_TACHYON_ANCHOR, + ), + ])?; + #[cfg(not(zcash_unstable = "nutachyon"))] extended.try_extend([ ( second_block.clone(), @@ -807,7 +882,27 @@ fn try_extend_forwards_ironwood_roots_in_order() -> Result<()> { &sapling_root, &orchard_root, &first_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; + #[cfg(zcash_unstable = "nutachyon")] + wrong_last_root.try_extend([ + ( + second_block, + &sapling_root, + &orchard_root, + &second_ironwood_root, + &DEFAULT_TACHYON_ANCHOR, + ), + ( + third_block, + &sapling_root, + &orchard_root, + &Default::default(), + &DEFAULT_TACHYON_ANCHOR, + ), + ])?; + #[cfg(not(zcash_unstable = "nutachyon"))] wrong_last_root.try_extend([ ( second_block, @@ -873,9 +968,13 @@ fn v3_pruning_and_hash_dispatch_match_unpruned_tree() -> Result<()> { sapling_root, orchard_root, ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: &DEFAULT_TACHYON_ANCHOR, sapling_tx: u64::from(height), orchard_tx: u64::from(height + 1), ironwood_tx: u64::from(height + 2), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tx: 0, } } @@ -987,14 +1086,25 @@ fn v3_clone_preserves_root_and_future_append() -> Result<()> { &sapling_root, &orchard_root, &first_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; original.push( block(2), &sapling_root, &orchard_root, &second_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), + )?; + original.push( + block(3), + &sapling_root, + &orchard_root, + &third_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; - original.push(block(3), &sapling_root, &orchard_root, &third_ironwood_root)?; assert_eq!(original.peaks().len(), 2, "three leaves have two MMR peaks"); let mut cloned = original.clone(); @@ -1013,12 +1123,16 @@ fn v3_clone_preserves_root_and_future_append() -> Result<()> { &sapling_root, &orchard_root, &fourth_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; cloned.push( fourth_block, &sapling_root, &orchard_root, &fourth_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; assert_eq!(cloned.hash(), original.hash()); @@ -1081,10 +1195,19 @@ fn history_tree_from_block_matches_parts_across_history_versions() -> Result<()> &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; let parts_tree = HistoryTree::from_parts( &network, - HistoryTreeBlockParts::from_block(&block, &sapling_root, &orchard_root, &ironwood_root), + HistoryTreeBlockParts::from_block( + &block, + &sapling_root, + &orchard_root, + &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), + ), )?; assert_eq!(block_tree.is_some(), should_exist); @@ -1138,6 +1261,8 @@ fn history_tree_push_wrapper_matches_parts_across_nu6_3_activation() -> Result<( &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; parts_tree.push_from_parts( &network, @@ -1146,6 +1271,8 @@ fn history_tree_push_wrapper_matches_parts_across_nu6_3_activation() -> Result<( &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ), )?; assert_eq!(block_tree.hash(), parts_tree.hash()); @@ -1156,6 +1283,8 @@ fn history_tree_push_wrapper_matches_parts_across_nu6_3_activation() -> Result<( &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; parts_tree.push_from_parts( &network, @@ -1164,6 +1293,8 @@ fn history_tree_push_wrapper_matches_parts_across_nu6_3_activation() -> Result<( &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ), )?; let activation_tree = HistoryTree::from_block( @@ -1172,6 +1303,8 @@ fn history_tree_push_wrapper_matches_parts_across_nu6_3_activation() -> Result<( &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; let activation_without_ironwood = HistoryTree::from_block( &network, @@ -1179,6 +1312,8 @@ fn history_tree_push_wrapper_matches_parts_across_nu6_3_activation() -> Result<( &sapling_root, &orchard_root, &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; assert_eq!(block_tree.hash(), parts_tree.hash()); @@ -1233,6 +1368,8 @@ fn upgrade_for_network_upgrade(network: Network, network_upgrade: NetworkUpgrade &sapling_root_prev, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; assert_eq!(tree.size(), 1); @@ -1260,6 +1397,8 @@ fn upgrade_for_network_upgrade(network: Network, network_upgrade: NetworkUpgrade &activation_sapling_root, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .unwrap(); diff --git a/crates/zakura-chain/src/lib.rs b/crates/zakura-chain/src/lib.rs index aa2d5104ee..00645c19bd 100644 --- a/crates/zakura-chain/src/lib.rs +++ b/crates/zakura-chain/src/lib.rs @@ -44,6 +44,8 @@ pub mod shutdown; pub mod sprout; pub mod subtree; pub mod subtree_verify; +#[cfg(zcash_unstable = "nutachyon")] +pub mod tachyon; pub mod transaction; pub mod transparent; pub mod value_balance; diff --git a/crates/zakura-chain/src/local_genesis.rs b/crates/zakura-chain/src/local_genesis.rs index c699a3d30c..7b3d38446e 100644 --- a/crates/zakura-chain/src/local_genesis.rs +++ b/crates/zakura-chain/src/local_genesis.rs @@ -439,6 +439,8 @@ fn configured_activation_heights( nu6_2: (latest_network_upgrade >= Nu6_2).then_some(activation_height), nu6_3: (latest_network_upgrade >= Nu6_3).then_some(activation_height), nu7: (latest_network_upgrade >= Nu7).then_some(activation_height), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: (latest_network_upgrade >= NuTachyon).then_some(activation_height), }) } diff --git a/crates/zakura-chain/src/parallel/commitment_aux_verify.rs b/crates/zakura-chain/src/parallel/commitment_aux_verify.rs index 015661ecac..ad187b8b2b 100644 --- a/crates/zakura-chain/src/parallel/commitment_aux_verify.rs +++ b/crates/zakura-chain/src/parallel/commitment_aux_verify.rs @@ -102,6 +102,11 @@ impl VerifiedHeaderCommitmentRoots { /// A supplied-root verification failure. #[derive(Debug, Error)] pub enum SuppliedRootsError { + /// The roots-only auxiliary format cannot reconstruct Tachyon history state. + #[error("Tachyon history commitments require block bodies")] + #[cfg(zcash_unstable = "nutachyon")] + TachyonDataUnavailable, + /// A header commitment did not match its supplied auxiliary data. #[error("invalid header commitment: {0}")] InvalidHeaderCommitment(#[from] CommitmentError), @@ -143,6 +148,11 @@ where for (index, (header, roots)) in items.iter().enumerate() { let height = roots.height; + #[cfg(zcash_unstable = "nutachyon")] + if NetworkUpgrade::current(network, height) >= NetworkUpgrade::NuTachyon { + return Err((height, SuppliedRootsError::TachyonDataUnavailable)); + } + header_commitment_is_valid_for_chain_history( header, height, @@ -183,9 +193,13 @@ where sapling_root: &roots.sapling_root, orchard_root: &roots.orchard_root, ironwood_root: &roots.ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: &Default::default(), sapling_tx: roots.sapling_tx, orchard_tx: roots.orchard_tx, ironwood_tx: roots.ironwood_tx, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tx: 0, }, ) .map_err(Arc::new) @@ -480,6 +494,8 @@ mod tests { &Default::default(), &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("empty history tree for a pre-Heartwood block") } @@ -914,6 +930,8 @@ mod tests { &act_root, &empty_orchard_root, &empty_ironwood_root(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("activation block builds a history tree") .hash(), @@ -940,6 +958,8 @@ mod tests { &empty_sapling_root, &empty_orchard_root, &empty_ironwood_root(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("the parent history tree builds"); let witness_roots = diff --git a/crates/zakura-chain/src/parallel/tree.rs b/crates/zakura-chain/src/parallel/tree.rs index 8d5c5e8138..09d7c25c0f 100644 --- a/crates/zakura-chain/src/parallel/tree.rs +++ b/crates/zakura-chain/src/parallel/tree.rs @@ -4,6 +4,8 @@ use std::sync::Arc; use thiserror::Error; +#[cfg(zcash_unstable = "nutachyon")] +use crate::tachyon; use crate::{ block::Block, ironwood, orchard, sapling, sprout, @@ -35,6 +37,14 @@ pub struct NoteCommitmentTrees { /// The Ironwood note commitment subtree. pub ironwood_subtree: Option>, + + /// The Tachyon pool anchor after this block. + #[cfg(zcash_unstable = "nutachyon")] + pub tachyon_anchor: tachyon::Anchor, + + /// The epoch-boundary anchor if this block starts a Tachyon epoch. + #[cfg(zcash_unstable = "nutachyon")] + pub tachyon_epoch_anchor: Option, } /// Note commitment tree errors. diff --git a/crates/zakura-chain/src/parameters/network/testnet.rs b/crates/zakura-chain/src/parameters/network/testnet.rs index 09c8146b56..ed8c69f1a3 100644 --- a/crates/zakura-chain/src/parameters/network/testnet.rs +++ b/crates/zakura-chain/src/parameters/network/testnet.rs @@ -194,6 +194,8 @@ impl From<&BTreeMap> for ConfiguredActivationHeights { NetworkUpgrade::Nu6_2 => &mut configured_activation_heights.nu6_2, NetworkUpgrade::Nu6_3 => &mut configured_activation_heights.nu6_3, NetworkUpgrade::Nu7 => &mut configured_activation_heights.nu7, + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => &mut configured_activation_heights.nu_tachyon, #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => &mut configured_activation_heights.zfuture, NetworkUpgrade::Genesis => continue, @@ -514,6 +516,9 @@ pub struct ConfiguredActivationHeights { /// Activation height for `NU7` network upgrade. #[serde(rename = "NU7")] pub nu7: Option, + /// Activation height for `NuTachyon` network upgrade. + #[cfg(zcash_unstable = "nutachyon")] + pub nu_tachyon: Option, /// Activation height for `ZFuture` network upgrade. #[serde(rename = "ZFuture")] #[cfg(zcash_unstable = "zfuture")] @@ -537,6 +542,8 @@ impl ConfiguredActivationHeights { nu6_2, nu6_3, nu7, + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon, #[cfg(zcash_unstable = "zfuture")] zfuture, } = self; @@ -560,6 +567,8 @@ impl ConfiguredActivationHeights { nu6_2, nu6_3, nu7, + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon, #[cfg(zcash_unstable = "zfuture")] zfuture, } @@ -757,6 +766,8 @@ impl ParametersBuilder { nu6_2, nu6_3, nu7, + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon, #[cfg(zcash_unstable = "zfuture")] zfuture, }: ConfiguredActivationHeights, @@ -787,6 +798,10 @@ impl ParametersBuilder { .chain(nu6_3.into_iter().map(|h| (h, Nu6_3))) .chain(nu7.into_iter().map(|h| (h, Nu7))); + #[cfg(zcash_unstable = "nutachyon")] + let activation_heights = + activation_heights.chain(nu_tachyon.into_iter().map(|h| (h, NuTachyon))); + #[cfg(zcash_unstable = "zfuture")] let activation_heights = activation_heights.chain(zfuture.into_iter().map(|h| (h, ZFuture))); diff --git a/crates/zakura-chain/src/parameters/network/tests/vectors.rs b/crates/zakura-chain/src/parameters/network/tests/vectors.rs index 5cfb2d9e11..0bf3e4ac47 100644 --- a/crates/zakura-chain/src/parameters/network/tests/vectors.rs +++ b/crates/zakura-chain/src/parameters/network/tests/vectors.rs @@ -222,10 +222,14 @@ fn bip70_network_names_are_stable_for_every_network_kind() { fn activates_network_upgrades_correctly() { let expected_nu6_3_activation_height = 1; let expected_nu7_activation_height = 2; + #[cfg(zcash_unstable = "nutachyon")] + let expected_nu_tachyon_activation_height = 3; let network = testnet::Parameters::build() .with_activation_heights(ConfiguredActivationHeights { nu6_3: Some(expected_nu6_3_activation_height), nu7: Some(expected_nu7_activation_height), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: Some(expected_nu_tachyon_activation_height), ..Default::default() }) .expect("failed to set activation heights") @@ -246,6 +250,8 @@ fn activates_network_upgrades_correctly() { for nu in NetworkUpgrade::iter().skip(1) { let expected_activation_height = match nu { NetworkUpgrade::Nu7 => expected_nu7_activation_height, + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => expected_nu_tachyon_activation_height, _ => expected_nu6_3_activation_height, }; @@ -340,6 +346,26 @@ fn configured_nu6_3_activation_preserves_upgrade_order() { "an overwritten NU6.3 activation must remain implicit at the NU7 height" ); + #[cfg(zcash_unstable = "nutachyon")] + { + let nu_tachyon_network = testnet::Parameters::build() + .with_activation_heights(ConfiguredActivationHeights { + nu7: Some(activation_height.0), + nu_tachyon: Some(activation_height.0), + ..Default::default() + }) + .expect("same-height upgrades are valid") + .clear_funding_streams() + .to_network() + .expect("valid configured network"); + + assert_eq!( + NetworkUpgrade::current(&nu_tachyon_network, activation_height), + NetworkUpgrade::NuTachyon, + "NuTachyon must overwrite NU7 when both activate at the same height" + ); + } + let out_of_order = testnet::Parameters::build() .with_activation_heights(ConfiguredActivationHeights { nu6_2: Some(activation_height.0 + 1), diff --git a/crates/zakura-chain/src/parameters/network_upgrade.rs b/crates/zakura-chain/src/parameters/network_upgrade.rs index 8fb7601f3b..a9968ef639 100644 --- a/crates/zakura-chain/src/parameters/network_upgrade.rs +++ b/crates/zakura-chain/src/parameters/network_upgrade.rs @@ -67,6 +67,9 @@ pub enum NetworkUpgrade { /// The Zcash protocol after the NU7 upgrade. #[serde(rename = "NU7")] Nu7, + /// The Zcash protocol after the NuTachyon upgrade. + #[cfg(zcash_unstable = "nutachyon")] + NuTachyon, #[cfg(zcash_unstable = "zfuture")] ZFuture, @@ -241,6 +244,8 @@ pub(crate) const CONSENSUS_BRANCH_IDS: &[(NetworkUpgrade, ConsensusBranchId)] = // TODO: set below to (Nu7, ConsensusBranchId(0x77190ad8)), once the same value is set in librustzcash #[cfg(any(test, feature = "zakura-test"))] (Nu7, ConsensusBranchId(0xfffffffe)), + #[cfg(zcash_unstable = "nutachyon")] + (NuTachyon, ConsensusBranchId(0xfffffffc)), #[cfg(zcash_unstable = "zfuture")] (ZFuture, ConsensusBranchId(0xfffffffd)), ]; @@ -406,6 +411,9 @@ impl NetworkUpgrade { POST_BLOSSOM_POW_TARGET_SPACING.into() } + #[cfg(zcash_unstable = "nutachyon")] + NuTachyon => POST_BLOSSOM_POW_TARGET_SPACING.into(), + #[cfg(zcash_unstable = "zfuture")] ZFuture => POST_BLOSSOM_POW_TARGET_SPACING.into(), }; @@ -531,6 +539,8 @@ impl From for NetworkUpgrade { zcash_protocol::consensus::NetworkUpgrade::Nu6_3 => Self::Nu6_3, #[cfg(zcash_unstable = "nu7")] zcash_protocol::consensus::NetworkUpgrade::Nu7 => Self::Nu7, + #[cfg(zcash_unstable = "nutachyon")] + zcash_protocol::consensus::NetworkUpgrade::NuTachyon => Self::NuTachyon, #[cfg(zcash_unstable = "zfuture")] zcash_protocol::consensus::NetworkUpgrade::ZFuture => Self::ZFuture, } diff --git a/crates/zakura-chain/src/parameters/tests.rs b/crates/zakura-chain/src/parameters/tests.rs index 6d5ec15a65..71f8c804e7 100644 --- a/crates/zakura-chain/src/parameters/tests.rs +++ b/crates/zakura-chain/src/parameters/tests.rs @@ -274,6 +274,8 @@ const NETWORK_UPGRADES_IN_ORDER: &[NetworkUpgrade] = &[ #[cfg(any(test, feature = "zakura-test"))] Nu6_3, Nu7, + #[cfg(zcash_unstable = "nutachyon")] + NuTachyon, ]; #[test] @@ -289,7 +291,9 @@ fn full_activation_list_contains_all_upgrades() { let network = Network::Mainnet; let full_list = network.full_activation_list(); - // NU7 is unscheduled on Mainnet (no activation height committed), so it is absent from the - // full activation list even though it is always present in the iter. + // Unscheduled upgrades are absent from the full activation list. + #[cfg(zcash_unstable = "nutachyon")] + assert_eq!(full_list.len(), NetworkUpgrade::iter().count() - 2); + #[cfg(not(zcash_unstable = "nutachyon"))] assert_eq!(full_list.len(), NetworkUpgrade::iter().count() - 1); } diff --git a/crates/zakura-chain/src/parameters/transaction.rs b/crates/zakura-chain/src/parameters/transaction.rs index b3342d380a..193703dfe9 100644 --- a/crates/zakura-chain/src/parameters/transaction.rs +++ b/crates/zakura-chain/src/parameters/transaction.rs @@ -14,3 +14,7 @@ pub const TX_V5_VERSION_GROUP_ID: u32 = 0x26A7_270A; /// The version group ID for version 6 transactions. pub const TX_V6_VERSION_GROUP_ID: u32 = 0xD884_B698; + +/// The placeholder version group ID for version 7 transactions ("tach" in ASCII). +#[cfg(zcash_unstable = "nutachyon")] +pub const TX_V7_VERSION_GROUP_ID: u32 = 0x7461_6368; diff --git a/crates/zakura-chain/src/primitives/zcash_history.rs b/crates/zakura-chain/src/primitives/zcash_history.rs index bc0d32c503..09f3789ed0 100644 --- a/crates/zakura-chain/src/primitives/zcash_history.rs +++ b/crates/zakura-chain/src/primitives/zcash_history.rs @@ -9,8 +9,12 @@ mod tests; use std::{collections::BTreeMap, io, sync::Arc}; use serde_big_array::BigArray; -pub use zcash_history::{V1, V2, V3}; +#[cfg(zcash_unstable = "nutachyon")] +pub use zcash_history::V4; +pub use zcash_history::{MAX_ENTRY_SIZE, V1, V2, V3}; +#[cfg(zcash_unstable = "nutachyon")] +use crate::tachyon; use crate::{ block::{Block, ChainHistoryMmrRootHash, Header, Height}, ironwood, orchard, @@ -31,12 +35,18 @@ pub struct HistoryTreeBlockParts<'a> { pub orchard_root: &'a orchard::tree::Root, /// The block's Ironwood note commitment tree root. pub ironwood_root: &'a ironwood::tree::Root, + /// The Tachyon pool anchor after the block. + #[cfg(zcash_unstable = "nutachyon")] + pub tachyon_anchor: &'a tachyon::Anchor, /// The number of Sapling transactions in the block. pub sapling_tx: u64, /// The number of Orchard transactions in the block. pub orchard_tx: u64, /// The number of Ironwood transactions in the block. pub ironwood_tx: u64, + /// The number of Tachyon transactions in the block. + #[cfg(zcash_unstable = "nutachyon")] + pub tachyon_tx: u64, } impl<'a> HistoryTreeBlockParts<'a> { @@ -46,6 +56,7 @@ impl<'a> HistoryTreeBlockParts<'a> { sapling_root: &'a sapling::tree::Root, orchard_root: &'a orchard::tree::Root, ironwood_root: &'a ironwood::tree::Root, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: &'a tachyon::Anchor, ) -> Self { let height = block .coinbase_height() @@ -57,9 +68,13 @@ impl<'a> HistoryTreeBlockParts<'a> { sapling_root, orchard_root, ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, sapling_tx: block.sapling_transactions_count(), orchard_tx: block.orchard_transactions_count(), ironwood_tx: block.ironwood_transactions_count(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tx: block.tachyon_transactions_count(), } } } @@ -73,9 +88,17 @@ pub trait Version: zcash_history::Version { sapling_root: &sapling::tree::Root, orchard_root: &orchard::tree::Root, ironwood_root: &ironwood::tree::Root, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: &tachyon::Anchor, ) -> Self::NodeData { Self::parts_to_history_node( - HistoryTreeBlockParts::from_block(&block, sapling_root, orchard_root, ironwood_root), + HistoryTreeBlockParts::from_block( + &block, + sapling_root, + orchard_root, + ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, + ), network, ) } @@ -146,9 +169,17 @@ impl Entry { sapling_root: &sapling::tree::Root, orchard_root: &orchard::tree::Root, ironwood_root: &ironwood::tree::Root, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: &tachyon::Anchor, ) -> Self { - let node_data = - V::block_to_history_node(block, network, sapling_root, orchard_root, ironwood_root); + let node_data = V::block_to_history_node( + block, + network, + sapling_root, + orchard_root, + ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, + ); Self::from_node_data::(node_data) } @@ -227,13 +258,21 @@ impl Tree { sapling_root: &sapling::tree::Root, orchard_root: &orchard::tree::Root, ironwood_root: &ironwood::tree::Root, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: &tachyon::Anchor, ) -> Result<(Self, Entry), io::Error> { let height = block .coinbase_height() .expect("block must have coinbase height during contextual verification"); let network_upgrade = NetworkUpgrade::current(network, height); - let entry0 = - Entry::new_leaf::(block, network, sapling_root, orchard_root, ironwood_root); + let entry0 = Entry::new_leaf::( + block, + network, + sapling_root, + orchard_root, + ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, + ); let mut peaks = BTreeMap::new(); peaks.insert(0u32, entry0); Ok(( @@ -283,6 +322,7 @@ impl Tree { sapling_root: &sapling::tree::Root, orchard_root: &orchard::tree::Root, ironwood_root: &ironwood::tree::Root, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: &tachyon::Anchor, ) -> Result, zcash_history::Error> { let height = block .coinbase_height() @@ -302,6 +342,8 @@ impl Tree { sapling_root, orchard_root, ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, ); let appended = self.inner.append_leaf(node_data)?; @@ -423,6 +465,8 @@ impl Version for zcash_history::V1 { | NetworkUpgrade::Nu6_2 | NetworkUpgrade::Nu6_3 | NetworkUpgrade::Nu7 => {} + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => {} #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => {} }; @@ -485,3 +529,20 @@ impl Version for V3 { } } } + +#[cfg(zcash_unstable = "nutachyon")] +impl Version for V4 { + fn parts_to_history_node( + parts: HistoryTreeBlockParts<'_>, + network: &Network, + ) -> Self::NodeData { + let node_data_v3 = V3::parts_to_history_node(parts, network); + let tachyon_anchor: [u8; 32] = parts.tachyon_anchor.into(); + Self::NodeData { + v3: node_data_v3, + start_tachyon_anchor: tachyon_anchor, + end_tachyon_anchor: tachyon_anchor, + tachyon_tx: parts.tachyon_tx, + } + } +} diff --git a/crates/zakura-chain/src/primitives/zcash_history/tests/vectors.rs b/crates/zakura-chain/src/primitives/zcash_history/tests/vectors.rs index 09220f5b43..95c2f8af34 100644 --- a/crates/zakura-chain/src/primitives/zcash_history/tests/vectors.rs +++ b/crates/zakura-chain/src/primitives/zcash_history/tests/vectors.rs @@ -14,6 +14,8 @@ const U32_SIZE: usize = 4; const V1_FIXED_NODE_DATA_SIZE: usize = HISTORY_HASH_SIZE + U32_SIZE * 4 + HISTORY_HASH_SIZE * 3; const V2_EXTRA_FIXED_NODE_DATA_SIZE: usize = HISTORY_HASH_SIZE * 2; const V3_EXTRA_FIXED_NODE_DATA_SIZE: usize = HISTORY_HASH_SIZE * 2; +#[cfg(zcash_unstable = "nutachyon")] +const V4_EXTRA_FIXED_NODE_DATA_SIZE: usize = HISTORY_HASH_SIZE * 2; /// Test the MMR tree using the activation block of a network upgrade /// and its next block. @@ -58,6 +60,8 @@ fn tree_for_network_upgrade(network: &Network, network_upgrade: NetworkUpgrade) &sapling_root0, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), )?; // Compute root hash of the MMR tree, which will be included in the next block @@ -88,6 +92,8 @@ fn tree_for_network_upgrade(network: &Network, network_upgrade: NetworkUpgrade) &sapling_root1, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .unwrap(); @@ -114,6 +120,8 @@ fn old_history_versions_ignore_ironwood_root() -> Result<()> { &sapling_root, &orchard_root, &default_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ); let v1_non_default = ::block_to_history_node( block.clone(), @@ -121,6 +129,8 @@ fn old_history_versions_ignore_ironwood_root() -> Result<()> { &sapling_root, &orchard_root, &non_default_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ); assert_eq!( @@ -138,6 +148,8 @@ fn old_history_versions_ignore_ironwood_root() -> Result<()> { &sapling_root, &orchard_root, &default_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ); let v2_non_default = ::block_to_history_node( block, @@ -145,6 +157,8 @@ fn old_history_versions_ignore_ironwood_root() -> Result<()> { &sapling_root, &orchard_root, &non_default_ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ); assert_eq!( @@ -173,6 +187,8 @@ fn v3_history_node_hash_input_has_exact_serialized_size() -> Result<()> { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ); assert_eq!(node_data.start_ironwood_root, ironwood_root_bytes); @@ -186,11 +202,48 @@ fn v3_history_node_hash_input_has_exact_serialized_size() -> Result<()> { // value in a V3 node. assert_eq!(encoded.len(), serialized_v3_node_data_size(&node_data)); assert_eq!(encoded.last(), Some(&1)); + #[cfg(zcash_unstable = "nutachyon")] + assert_eq!(::zcash_history::MAX_NODE_DATA_SIZE, 390); + #[cfg(not(zcash_unstable = "nutachyon"))] assert_eq!(::zcash_history::MAX_NODE_DATA_SIZE, 317); Ok(()) } +#[test] +#[cfg(zcash_unstable = "nutachyon")] +fn v4_history_node_commits_to_tachyon_state() -> Result<()> { + let network = Network::Mainnet; + let (block, sapling_root) = block_and_sapling_root(&network, NetworkUpgrade::Canopy)?; + let orchard_root = orchard::tree::Root::default(); + let ironwood_root = non_default_ironwood_root(); + let tachyon_anchor = crate::tachyon::Anchor([1; 32]); + + let mut node_data = ::block_to_history_node( + block, + &network, + &sapling_root, + &orchard_root, + &ironwood_root, + &tachyon_anchor, + ); + + assert_eq!(node_data.start_tachyon_anchor, tachyon_anchor.0); + assert_eq!(node_data.end_tachyon_anchor, tachyon_anchor.0); + + let without_tachyon_transaction = ::hash(&node_data); + node_data.tachyon_tx = 1; + let with_tachyon_transaction = ::hash(&node_data); + assert_ne!(without_tachyon_transaction, with_tachyon_transaction); + + let encoded = ::to_bytes(&node_data); + assert_eq!(encoded.len(), serialized_v4_node_data_size(&node_data)); + assert_eq!(encoded.last(), Some(&1)); + assert_eq!(::zcash_history::MAX_NODE_DATA_SIZE, 390); + + Ok(()) +} + fn block_and_sapling_root( network: &Network, network_upgrade: NetworkUpgrade, @@ -231,6 +284,13 @@ fn serialized_v3_node_data_size(data: &::zcash_history::NodeDataV3) -> usize { + compact_size(data.ironwood_tx) } +#[cfg(zcash_unstable = "nutachyon")] +fn serialized_v4_node_data_size(data: &::zcash_history::NodeDataV4) -> usize { + serialized_v3_node_data_size(&data.v3) + + V4_EXTRA_FIXED_NODE_DATA_SIZE + + compact_size(data.tachyon_tx) +} + fn serialized_v2_node_data_size(data: &::zcash_history::NodeDataV2) -> usize { serialized_v1_node_data_size(&data.v1) + V2_EXTRA_FIXED_NODE_DATA_SIZE diff --git a/crates/zakura-chain/src/tachyon.rs b/crates/zakura-chain/src/tachyon.rs new file mode 100644 index 0000000000..de88e95539 --- /dev/null +++ b/crates/zakura-chain/src/tachyon.rs @@ -0,0 +1,215 @@ +//! Tachyon shielded-pool chain-state types. + +use std::{fmt, io}; + +use serde::{Deserialize, Serialize}; + +use crate::{ + block::Block, + parameters::{Network, NetworkUpgrade}, + serialization::{ReadZcashExt, SerializationError, ZcashDeserialize, ZcashSerialize}, +}; + +/// The number of blocks in a Tachyon epoch. +pub const EPOCH_LENGTH: u32 = zcash_tachyon::constants::EPOCH_SIZE; + +/// Returns the zero-based Tachyon pool height at `height`. +pub fn pool_height(network: &Network, height: crate::block::Height) -> Option { + let activation_height = NetworkUpgrade::NuTachyon.activation_height(network)?; + height.0.checked_sub(activation_height.0) +} + +/// Returns the Tachyon epoch containing `pool_height`. +pub fn epoch_of_pool_height(pool_height: u32) -> u32 { + pool_height / EPOCH_LENGTH +} + +/// Returns `true` if `pool_height` is the first block in its epoch. +pub fn is_epoch_first(pool_height: u32) -> bool { + pool_height.is_multiple_of(EPOCH_LENGTH) +} + +/// Returns the Tachyon epoch containing `height`, if the pool is active. +pub fn epoch(network: &Network, height: crate::block::Height) -> Option { + pool_height(network, height).map(epoch_of_pool_height) +} + +/// Returns `true` if `earlier` is in `later`'s two-epoch consensus scan window. +pub fn within_scan_window( + network: &Network, + earlier: crate::block::Height, + later: crate::block::Height, +) -> bool { + match (epoch(network, earlier), epoch(network, later)) { + (Some(earlier_epoch), Some(later_epoch)) => earlier_epoch + 1 >= later_epoch, + _ => false, + } +} + +/// The running Tachyon pool anchor after a block. +#[derive(Clone, Copy, Default, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Anchor(pub [u8; 32]); + +impl fmt::Debug for Anchor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("tachyon::Anchor") + .field(&hex::encode(self.0)) + .finish() + } +} + +impl From<[u8; 32]> for Anchor { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl From for [u8; 32] { + fn from(anchor: Anchor) -> Self { + anchor.0 + } +} + +impl From<&Anchor> for [u8; 32] { + fn from(anchor: &Anchor) -> Self { + anchor.0 + } +} + +impl ZcashSerialize for Anchor { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&self.0) + } +} + +impl ZcashDeserialize for Anchor { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(Self(reader.read_32_bytes()?)) + } +} + +impl From for Anchor { + fn from(anchor: zcash_tachyon::Anchor) -> Self { + let mut bytes = Vec::with_capacity(32); + anchor + .write(&mut bytes) + .expect("serializing a Tachyon anchor into a Vec is infallible"); + Self( + bytes + .try_into() + .expect("Tachyon anchors always encode as 32 bytes"), + ) + } +} + +/// A nullifier or note commitment revealed by a Tachyon proof stamp. +#[derive(Clone, Copy, Eq, PartialEq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Tachygram(pub [u8; 32]); + +impl fmt::Debug for Tachygram { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("tachyon::Tachygram") + .field(&hex::encode(self.0)) + .finish() + } +} + +impl From<[u8; 32]> for Tachygram { + fn from(bytes: [u8; 32]) -> Self { + Self(bytes) + } +} + +impl From for [u8; 32] { + fn from(tachygram: Tachygram) -> Self { + tachygram.0 + } +} + +impl From<&Tachygram> for [u8; 32] { + fn from(tachygram: &Tachygram) -> Self { + tachygram.0 + } +} + +impl From for Tachygram { + fn from(tachygram: zcash_tachyon::Tachygram) -> Self { + let field: halo2::pasta::pallas::Base = tachygram.into(); + Self(field.into()) + } +} + +impl Anchor { + fn to_tachyon(self) -> zcash_tachyon::Anchor { + zcash_tachyon::Anchor::read(&self.0[..]) + .expect("stored Tachyon anchors are canonical field-element encodings") + } + + /// Computes the Tachyon pool anchor after `block`. + pub fn advance_with_block( + &self, + pool_height: u32, + block: &Block, + ) -> Result { + use zcash_tachyon::TachyonBundle; + + let epoch = zcash_tachyon::EpochIndex(epoch_of_pool_height(pool_height)); + let mut anchor = if pool_height == 0 { + zcash_tachyon::Anchor::default() + } else { + self.to_tachyon() + }; + let epoch_boundary = if pool_height == 0 { + Some(Anchor::from(anchor)) + } else if is_epoch_first(pool_height) { + anchor = anchor.next_epoch(epoch)?; + Some(Anchor::from(anchor)) + } else { + None + }; + + for transaction in &block.transactions { + let Some(shielded_data) = transaction.tachyon_shielded_data() else { + continue; + }; + if let TachyonBundle::Proven(bundle) = &shielded_data.0 { + anchor = anchor.next_stamp(epoch, &bundle.stamp.tachygram_set)?; + } + } + + Ok(AnchorAdvance { + post_block: Anchor::from(anchor), + epoch_boundary, + }) + } +} + +/// The result of advancing the Tachyon anchor through a block. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AnchorAdvance { + /// The anchor after the block. + pub post_block: Anchor, + /// The anchor immediately after an epoch lift, if this block starts an epoch. + pub epoch_boundary: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn anchor_serialization_round_trip() { + let anchor = Anchor([42; 32]); + let mut bytes = Vec::new(); + anchor.zcash_serialize(&mut bytes).unwrap(); + assert_eq!(Anchor::zcash_deserialize(&bytes[..]).unwrap(), anchor); + } + + #[test] + fn epoch_helpers_follow_epoch_length() { + assert_eq!(epoch_of_pool_height(0), 0); + assert_eq!(epoch_of_pool_height(EPOCH_LENGTH), 1); + assert!(is_epoch_first(0)); + assert!(is_epoch_first(EPOCH_LENGTH)); + } +} diff --git a/crates/zakura-chain/src/transaction.rs b/crates/zakura-chain/src/transaction.rs index 53d518a6d3..8f803a2102 100644 --- a/crates/zakura-chain/src/transaction.rs +++ b/crates/zakura-chain/src/transaction.rs @@ -11,6 +11,8 @@ mod lock_time; mod memo; mod serialize; mod sighash; +#[cfg(zcash_unstable = "nutachyon")] +mod tachyon_shielded; mod txid; mod unmined; pub(crate) mod zip244; @@ -33,12 +35,16 @@ pub use serialize::{ MIN_TRANSPARENT_TX_V5_SIZE, }; pub use sighash::{HashType, SigHash, SigHasher}; +#[cfg(zcash_unstable = "nutachyon")] +pub use tachyon_shielded::TachyonShieldedData; pub use unmined::{ zip317, UnminedTx, UnminedTxId, VerifiedUnminedTx, MEMPOOL_TRANSACTION_COST_THRESHOLD, }; use zcash_protocol::consensus; use crate::parameters::TX_V6_VERSION_GROUP_ID; +#[cfg(zcash_unstable = "nutachyon")] +use crate::parameters::TX_V7_VERSION_GROUP_ID; use crate::{ amount::{Amount, Error as AmountError, NegativeAllowed, NonNegative}, block, ironwood, @@ -169,6 +175,30 @@ pub enum Transaction { /// The Ironwood data for this transaction, if any. ironwood_shielded_data: Option, }, + /// A `version = 7` transaction enabled by NuTachyon. + #[cfg(zcash_unstable = "nutachyon")] + V7 { + /// The Network Upgrade for this transaction. + /// + /// Derived from the ConsensusBranchId field. + network_upgrade: NetworkUpgrade, + /// The earliest time or block height that this transaction can be added to the chain. + lock_time: LockTime, + /// The latest block height that this transaction can be added to the chain. + expiry_height: block::Height, + /// The transparent inputs to the transaction. + inputs: Vec, + /// The transparent outputs from the transaction. + outputs: Vec, + /// 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, + /// The Ironwood data for this transaction, if any. + ironwood_shielded_data: Option, + /// The Tachyon data for this transaction, if any. + tachyon_shielded_data: Option, + }, } impl AttributedMemorySize for Transaction { @@ -257,6 +287,37 @@ impl AttributedMemorySize for Transaction { .map_or(0, AttributedMemorySize::attributed_memory_size_bytes), ), ), + #[cfg(zcash_unstable = "nutachyon")] + V7 { + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + ironwood_shielded_data, + tachyon_shielded_data, + .. + } => ( + inputs, + outputs, + sapling_shielded_data + .as_ref() + .map_or(0, AttributedMemorySize::attributed_memory_size_bytes) + .saturating_add( + orchard_shielded_data + .as_ref() + .map_or(0, AttributedMemorySize::attributed_memory_size_bytes), + ) + .saturating_add( + ironwood_shielded_data + .as_ref() + .map_or(0, AttributedMemorySize::attributed_memory_size_bytes), + ) + .saturating_add( + tachyon_shielded_data + .as_ref() + .map_or(0, AttributedMemorySize::attributed_memory_size_bytes), + ), + ), }; vec_capacity_bytes(inputs) @@ -389,6 +450,8 @@ impl Transaction { | Transaction::V4 { .. } => None, Transaction::V5 { .. } => Some(AuthDigest::from(self)), Transaction::V6 { .. } => Some(AuthDigest::from(self)), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Some(AuthDigest::from(self)), } } @@ -413,6 +476,12 @@ impl Transaction { crate::primitives::zcash_primitives::txid_and_auth_digest(self); (txid, Some(auth_digest)) } + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => { + let (txid, auth_digest) = + crate::primitives::zcash_primitives::txid_and_auth_digest(self); + (txid, Some(auth_digest)) + } } } @@ -454,6 +523,7 @@ impl Transaction { .ironwood_flags() .unwrap_or_else(ironwood::Flags::empty) .contains(ironwood::Flags::ENABLE_SPENDS)) + || self.has_tachyon_actions() } /// Does this transaction have shielded outputs? @@ -472,6 +542,7 @@ impl Transaction { .ironwood_flags() .unwrap_or_else(ironwood::Flags::empty) .contains(ironwood::Flags::ENABLE_OUTPUTS)) + || self.has_tachyon_actions() } /// Does this transaction have transparent or shielded outputs? @@ -524,6 +595,8 @@ impl Transaction { Transaction::V1 { .. } | Transaction::V2 { .. } => false, Transaction::V3 { .. } | Transaction::V4 { .. } | Transaction::V5 { .. } => true, Transaction::V6 { .. } => true, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => true, } } @@ -546,6 +619,8 @@ impl Transaction { Transaction::V4 { .. } => 4, Transaction::V5 { .. } => 5, Transaction::V6 { .. } => 6, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => 7, } } @@ -558,6 +633,8 @@ impl Transaction { | Transaction::V4 { lock_time, .. } | Transaction::V5 { lock_time, .. } => *lock_time, Transaction::V6 { lock_time, .. } => *lock_time, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { lock_time, .. } => *lock_time, }; // `zcashd` checks that the block height is greater than the lock height. @@ -606,6 +683,8 @@ impl Transaction { | Transaction::V4 { lock_time, .. } | Transaction::V5 { lock_time, .. } => *lock_time, Transaction::V6 { lock_time, .. } => *lock_time, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { lock_time, .. } => *lock_time, }; let mut lock_time_bytes = Vec::new(); lock_time @@ -642,11 +721,18 @@ impl Transaction { block::Height(0) => None, block::Height(expiry_height) => Some(block::Height(*expiry_height)), }, - Transaction::V6 { expiry_height, .. } => match expiry_height { - // # Consensus - // - // > No limit: To set no limit on transactions (so that they do not expire), nExpiryHeight should be set to 0. - // https://zips.z.cash/zip-0203#specification + Transaction::V6 { expiry_height, .. } => { + match expiry_height { + // # Consensus + // + // > No limit: To set no limit on transactions (so that they do not expire), nExpiryHeight should be set to 0. + // https://zips.z.cash/zip-0203#specification + block::Height(0) => None, + block::Height(expiry_height) => Some(block::Height(*expiry_height)), + } + } + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { expiry_height, .. } => match expiry_height { block::Height(0) => None, block::Height(expiry_height) => Some(block::Height(*expiry_height)), }, @@ -669,6 +755,10 @@ impl Transaction { Transaction::V6 { network_upgrade, .. } => Some(*network_upgrade), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + network_upgrade, .. + } => Some(*network_upgrade), } } @@ -683,6 +773,8 @@ impl Transaction { Transaction::V4 { ref inputs, .. } => inputs, Transaction::V5 { ref inputs, .. } => inputs, Transaction::V6 { ref inputs, .. } => inputs, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { ref inputs, .. } => inputs, } } @@ -702,6 +794,8 @@ impl Transaction { Transaction::V4 { ref outputs, .. } => outputs, Transaction::V5 { ref outputs, .. } => outputs, Transaction::V6 { ref outputs, .. } => outputs, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { ref outputs, .. } => outputs, } } @@ -751,6 +845,8 @@ impl Transaction { } | Transaction::V5 { .. } => Box::new(std::iter::empty()), Transaction::V6 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Box::new(std::iter::empty()), } } @@ -787,6 +883,8 @@ impl Transaction { } | Transaction::V5 { .. } => Box::new(std::iter::empty()), Transaction::V6 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Box::new(std::iter::empty()), } } @@ -823,6 +921,8 @@ impl Transaction { } | Transaction::V5 { .. } => 0, Transaction::V6 { .. } => 0, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => 0, } } @@ -863,6 +963,8 @@ impl Transaction { } | Transaction::V5 { .. } => Box::new(std::iter::empty()), Transaction::V6 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Box::new(std::iter::empty()), } } @@ -900,6 +1002,8 @@ impl Transaction { } | Transaction::V5 { .. } => None, Transaction::V6 { .. } => None, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => None, } } @@ -909,6 +1013,8 @@ impl Transaction { // No JoinSplits Transaction::V1 { .. } | Transaction::V5 { .. } => false, Transaction::V6 { .. } => false, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => false, // JoinSplits-on-BCTV14 Transaction::V2 { joinsplit_data, .. } | Transaction::V3 { joinsplit_data, .. } => { @@ -957,6 +1063,8 @@ impl Transaction { | Transaction::V1 { .. } | Transaction::V5 { .. } => Box::new(std::iter::empty()), Transaction::V6 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Box::new(std::iter::empty()), } } @@ -974,6 +1082,11 @@ impl Transaction { sapling_shielded_data, .. } => sapling_shielded_data.as_ref(), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data, + .. + } => sapling_shielded_data.as_ref(), _ => None, } } @@ -998,6 +1111,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.anchors()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.anchors()), // No Spends Transaction::V1 { .. } @@ -1015,6 +1133,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -1044,6 +1167,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.spends_per_anchor()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.spends_per_anchor()), // No Spends Transaction::V1 { .. } @@ -1061,6 +1189,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -1080,6 +1213,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.outputs()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.outputs()), // No Outputs Transaction::V1 { .. } @@ -1097,6 +1235,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -1118,6 +1261,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.nullifiers()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.nullifiers()), // No Spends Transaction::V1 { .. } @@ -1135,6 +1283,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -1158,6 +1311,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Box::new(sapling_shielded_data.note_commitments()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Box::new(sapling_shielded_data.note_commitments()), // No Spends Transaction::V1 { .. } @@ -1175,6 +1333,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: None, + .. + } => Box::new(std::iter::empty()), } } @@ -1194,6 +1357,11 @@ impl Transaction { sapling_shielded_data, .. } => sapling_shielded_data.is_some(), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data, + .. + } => sapling_shielded_data.is_some(), } } @@ -1219,6 +1387,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => sapling_shielded_data.point_encodings_are_valid(), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => sapling_shielded_data.point_encodings_are_valid(), _ => true, } } @@ -1238,6 +1411,11 @@ impl Transaction { orchard_shielded_data, .. } => orchard_shielded_data.as_ref(), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + orchard_shielded_data, + .. + } => orchard_shielded_data.as_ref(), // No Orchard shielded data Transaction::V1 { .. } @@ -1294,6 +1472,11 @@ impl Transaction { ironwood_shielded_data, .. } => ironwood_shielded_data.as_ref(), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + ironwood_shielded_data, + .. + } => ironwood_shielded_data.as_ref(), Transaction::V1 { .. } | Transaction::V2 { .. } @@ -1340,6 +1523,57 @@ impl Transaction { self.ironwood_shielded_data().is_some() } + // Tachyon + + /// Access the Tachyon shielded data in this transaction, if present. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_shielded_data(&self) -> Option<&TachyonShieldedData> { + match self { + Transaction::V7 { + tachyon_shielded_data, + .. + } => tachyon_shielded_data.as_ref(), + _ => None, + } + } + + /// Returns `true` if this transaction carries a Tachyon bundle. + #[cfg(zcash_unstable = "nutachyon")] + pub fn has_tachyon_shielded_data(&self) -> bool { + self.tachyon_shielded_data().is_some() + } + + /// Returns the tachygrams revealed by this transaction's proof stamp. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_tachygrams(&self) -> Vec { + match self.tachyon_shielded_data() { + Some(shielded_data) => match &shielded_data.0 { + zcash_tachyon::TachyonBundle::Proven(bundle) => bundle + .stamp + .tachygrams + .iter() + .copied() + .map(crate::tachyon::Tachygram::from) + .collect(), + zcash_tachyon::TachyonBundle::NoBundle + | zcash_tachyon::TachyonBundle::Adjunct(_) => Vec::new(), + }, + None => Vec::new(), + } + } + + /// Returns `true` if this transaction has any Tachyon actions. + #[cfg(zcash_unstable = "nutachyon")] + pub fn has_tachyon_actions(&self) -> bool { + self.tachyon_shielded_data() + .is_some_and(|data| !data.actions().is_empty()) + } + + #[cfg(not(zcash_unstable = "nutachyon"))] + fn has_tachyon_actions(&self) -> bool { + false + } + // value balances /// Return the transparent value balance, @@ -1452,6 +1686,8 @@ impl Transaction { } | Transaction::V5 { .. } => Box::new(std::iter::empty()), Transaction::V6 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Box::new(std::iter::empty()), } } @@ -1500,6 +1736,8 @@ impl Transaction { } | Transaction::V5 { .. } => Box::new(std::iter::empty()), Transaction::V6 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Box::new(std::iter::empty()), } } @@ -1542,6 +1780,8 @@ impl Transaction { } | Transaction::V5 { .. } => Box::new(iter::empty()), Transaction::V6 { .. } => Box::new(iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Box::new(iter::empty()), }; joinsplit_value_balances.map(ValueBalance::from_sprout_amount) @@ -1587,6 +1827,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => sapling_shielded_data.value_balance, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => sapling_shielded_data.value_balance, Transaction::V1 { .. } | Transaction::V2 { .. } @@ -1603,6 +1848,11 @@ impl Transaction { sapling_shielded_data: None, .. } => Amount::zero(), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: None, + .. + } => Amount::zero(), }; ValueBalance::from_sapling_amount(sapling_value_balance) @@ -1626,6 +1876,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Some(sapling_shielded_data.binding_sig), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Some(sapling_shielded_data.binding_sig), _ => None, } } @@ -1713,6 +1968,26 @@ impl Transaction { ValueBalance::from_ironwood_amount(ironwood_value_balance) } + /// Return the Tachyon value balance, the change in the transaction value pool due to + /// Tachyon actions. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_value_balance(&self) -> ValueBalance { + let tachyon_value_balance = self + .tachyon_shielded_data() + .map(|shielded_data| { + Amount::try_from(i64::from(shielded_data.value_balance())) + .expect("Tachyon value balances are range-checked when the bundle is parsed") + }) + .unwrap_or_else(Amount::zero); + + ValueBalance::from_tachyon_amount(tachyon_value_balance) + } + + #[cfg(not(zcash_unstable = "nutachyon"))] + fn tachyon_value_balance(&self) -> ValueBalance { + ValueBalance::zero() + } + /// Returns the value balances for this transaction using the provided transparent outputs. #[cfg(any(test, feature = "proptest-impl"))] pub(crate) fn value_balance_from_outputs( @@ -1724,6 +1999,7 @@ impl Transaction { + self.sapling_value_balance() + self.orchard_value_balance() + self.ironwood_value_balance() + + self.tachyon_value_balance() } /// Returns the value balances for this transaction. @@ -1782,6 +2058,7 @@ impl Transaction { + self.sapling_value_balance() + self.orchard_value_balance() + self.ironwood_value_balance() + + self.tachyon_value_balance() } /// Converts [`Transaction`] to [`zcash_primitives::transaction::Transaction`]. @@ -1829,6 +2106,8 @@ impl Transaction { Transaction::V4 { .. } => Some(SAPLING_VERSION_GROUP_ID), Transaction::V5 { .. } => Some(TX_V5_VERSION_GROUP_ID), Transaction::V6 { .. } => Some(TX_V6_VERSION_GROUP_ID), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Some(TX_V7_VERSION_GROUP_ID), } } } @@ -1862,6 +2141,14 @@ impl Transaction { *network_upgrade = nu; Ok(()) } + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + ref mut network_upgrade, + .. + } => { + *network_upgrade = nu; + Ok(()) + } } } @@ -1891,6 +2178,11 @@ impl Transaction { ref mut expiry_height, .. } => expiry_height, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + ref mut expiry_height, + .. + } => expiry_height, } } @@ -1903,6 +2195,8 @@ impl Transaction { Transaction::V4 { ref mut inputs, .. } => inputs, Transaction::V5 { ref mut inputs, .. } => inputs, Transaction::V6 { ref mut inputs, .. } => inputs, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { ref mut inputs, .. } => inputs, } } @@ -1933,6 +2227,11 @@ impl Transaction { sapling_shielded_data: Some(sapling_shielded_data), .. } => Some(&mut sapling_shielded_data.value_balance), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: Some(sapling_shielded_data), + .. + } => Some(&mut sapling_shielded_data.value_balance), Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } @@ -1948,6 +2247,11 @@ impl Transaction { sapling_shielded_data: None, .. } => None, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data: None, + .. + } => None, } } @@ -1997,6 +2301,8 @@ impl Transaction { } | Transaction::V5 { .. } => Box::new(std::iter::empty()), Transaction::V6 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Box::new(std::iter::empty()), } } @@ -2046,6 +2352,8 @@ impl Transaction { } | Transaction::V5 { .. } => Box::new(std::iter::empty()), Transaction::V6 { .. } => Box::new(std::iter::empty()), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Box::new(std::iter::empty()), } } @@ -2068,6 +2376,11 @@ impl Transaction { orchard_shielded_data: Some(orchard_shielded_data), .. } => Some(orchard_shielded_data), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + orchard_shielded_data: Some(orchard_shielded_data), + .. + } => Some(orchard_shielded_data), Transaction::V1 { .. } | Transaction::V2 { .. } @@ -2081,6 +2394,11 @@ impl Transaction { orchard_shielded_data: None, .. } => None, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + orchard_shielded_data: None, + .. + } => None, } } @@ -2105,6 +2423,10 @@ impl Transaction { Transaction::V6 { ref mut outputs, .. } => outputs, + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + ref mut outputs, .. + } => outputs, } } } diff --git a/crates/zakura-chain/src/transaction/arbitrary.rs b/crates/zakura-chain/src/transaction/arbitrary.rs index ec3a8b0b8f..4b20cca4ed 100644 --- a/crates/zakura-chain/src/transaction/arbitrary.rs +++ b/crates/zakura-chain/src/transaction/arbitrary.rs @@ -806,6 +806,8 @@ impl Arbitrary for Transaction { ] .boxed(), NetworkUpgrade::Nu7 => Self::v5_strategy(ledger_state), + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => Self::v5_strategy(ledger_state), #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => Self::v5_strategy(ledger_state), @@ -955,6 +957,8 @@ pub fn transaction_to_fake_v5( }, v5 @ V5 { .. } => v5.clone(), v6 @ V6 { .. } => v6.clone(), + #[cfg(zcash_unstable = "nutachyon")] + v7 @ V7 { .. } => v7.clone(), } } @@ -1040,6 +1044,8 @@ pub fn v5_transactions<'b>( | Transaction::V4 { .. } => None, ref tx @ Transaction::V5 { .. } => Some(tx.clone()), ref tx @ Transaction::V6 { .. } => Some(tx.clone()), + #[cfg(zcash_unstable = "nutachyon")] + ref tx @ Transaction::V7 { .. } => Some(tx.clone()), }) } diff --git a/crates/zakura-chain/src/transaction/hash.rs b/crates/zakura-chain/src/transaction/hash.rs index 75fe43fea5..259fe812d1 100644 --- a/crates/zakura-chain/src/transaction/hash.rs +++ b/crates/zakura-chain/src/transaction/hash.rs @@ -6,10 +6,10 @@ //! * [`WtxId`]: a 64-byte witnessed transaction ID, which uniquely identifies unmined transactions //! (transactions that are sent by wallets or stored in node mempools). //! -//! Transaction versions 5 and 6 use both these unique identifiers: -//! * [`struct@Hash`] uniquely identifies the effects of a v5 or v6 transaction (spends and outputs), +//! Transaction versions 5 and later use both these unique identifiers: +//! * [`struct@Hash`] uniquely identifies the effects of a v5+ transaction (spends and outputs), //! so it uniquely identifies the transaction's data after it has been mined into a block; -//! * [`WtxId`] uniquely identifies the effects and authorizing data of a v5 or v6 transaction +//! * [`WtxId`] uniquely identifies the effects and authorizing data of a v5+ transaction //! (signatures, proofs, and scripts), so it uniquely identifies the transaction's data //! outside a block. (For example, transactions produced by Zcash wallets, or in node mempools.) //! @@ -42,7 +42,7 @@ use crate::serialization::{ use super::{txid::TxIdBuilder, AuthDigest, Transaction}; -/// A transaction ID, which uniquely identifies mined v5 and v6 transactions, +/// A transaction ID, which uniquely identifies mined v5+ transactions, /// and all v1-v4 transactions. /// /// Note: Zebra displays transaction and block hashes in big-endian byte-order, @@ -195,7 +195,7 @@ impl ZcashDeserialize for Hash { } } -/// A witnessed transaction ID, which uniquely identifies unmined v5 and v6 transactions. +/// A witnessed transaction ID, which uniquely identifies unmined v5+ transactions. /// /// Witnessed transaction IDs are not used for transaction versions 1-4. /// diff --git a/crates/zakura-chain/src/transaction/serialize.rs b/crates/zakura-chain/src/transaction/serialize.rs index 0e5f9ad582..9cb246913a 100644 --- a/crates/zakura-chain/src/transaction/serialize.rs +++ b/crates/zakura-chain/src/transaction/serialize.rs @@ -22,6 +22,8 @@ use crate::{ }; use crate::parameters::TX_V6_VERSION_GROUP_ID; +#[cfg(zcash_unstable = "nutachyon")] +use crate::parameters::TX_V7_VERSION_GROUP_ID; use super::*; use crate::sapling; @@ -199,7 +201,7 @@ impl ZcashDeserialize for Option> { } } -/// Deserialize V5/V6 Sapling shielded data with an optional early coinbase +/// Deserialize V5+ Sapling shielded data with an optional early coinbase /// rejection. /// /// When `is_coinbase` is true, a non-zero `nSpendsSapling` count is rejected @@ -875,6 +877,54 @@ impl ZcashSerialize for Transaction { ALLOW_CROSS_ADDRESS_BIT, )?; } + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + ironwood_shielded_data, + tachyon_shielded_data, + } => { + if *network_upgrade < NetworkUpgrade::NuTachyon { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "v7 transaction must have a NuTachyon or later consensus branch ID", + )); + } + + writer.write_u32::(TX_V7_VERSION_GROUP_ID)?; + writer.write_u32::(u32::from( + network_upgrade + .branch_id() + .expect("valid transactions must have a network upgrade with a branch id"), + ))?; + lock_time.zcash_serialize(&mut writer)?; + writer.write_u32::(expiry_height.0)?; + inputs.zcash_serialize(&mut writer)?; + outputs.zcash_serialize(&mut writer)?; + sapling_shielded_data.zcash_serialize(&mut writer)?; + serialize_optional_orchard_shielded_data_with_flags( + orchard_shielded_data, + &mut writer, + !ALLOW_CROSS_ADDRESS_BIT, + )?; + serialize_optional_orchard_shielded_data_with_flags( + ironwood_shielded_data, + &mut writer, + ALLOW_CROSS_ADDRESS_BIT, + )?; + let tachyon_bundle = tachyon_shielded_data + .as_ref() + .map_or(&zcash_tachyon::TachyonBundle::NoBundle, |bundle| &bundle.0); + zcash_primitives::transaction::components::tachyon::write_v7_bundle( + tachyon_bundle, + &mut writer, + )?; + } } Ok(()) } @@ -1235,6 +1285,57 @@ impl ZcashDeserialize for Transaction { ironwood_shielded_data, }) } + #[cfg(zcash_unstable = "nutachyon")] + (7, true) => { + let id = limited_reader.read_u32::()?; + if id != TX_V7_VERSION_GROUP_ID { + return Err(SerializationError::Parse("expected TX_V7_VERSION_GROUP_ID")); + } + + let network_upgrade = + NetworkUpgrade::try_from(limited_reader.read_u32::()?)?; + if network_upgrade < NetworkUpgrade::NuTachyon { + return Err(SerializationError::Parse( + "v7 transaction must have a NuTachyon or later consensus branch ID", + )); + } + + let lock_time = LockTime::zcash_deserialize(&mut limited_reader)?; + let expiry_height = block::Height(limited_reader.read_u32::()?); + let inputs: Vec = Vec::zcash_deserialize(&mut limited_reader)?; + let outputs = Vec::zcash_deserialize(&mut limited_reader)?; + + let is_coinbase = inputs.len() == 1 + && matches!(inputs.first(), Some(transparent::Input::Coinbase { .. })); + let sapling_shielded_data = + deserialize_v5_sapling_shielded_data(&mut limited_reader, is_coinbase)?; + let orchard_shielded_data = deserialize_orchard_shielded_data_with_flags( + &mut limited_reader, + !ALLOW_CROSS_ADDRESS_BIT, + )?; + let ironwood_shielded_data = deserialize_orchard_shielded_data_with_flags( + &mut limited_reader, + ALLOW_CROSS_ADDRESS_BIT, + )?; + let tachyon_shielded_data = + match zcash_primitives::transaction::components::tachyon::read_v7_bundle( + &mut limited_reader, + )? { + zcash_tachyon::TachyonBundle::NoBundle => None, + bundle => Some(TachyonShieldedData::from(bundle)), + }; + Ok(Transaction::V7 { + network_upgrade, + lock_time, + expiry_height, + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + ironwood_shielded_data, + tachyon_shielded_data, + }) + } (_, _) => Err(SerializationError::Parse("bad tx header")), } } diff --git a/crates/zakura-chain/src/transaction/tachyon_shielded.rs b/crates/zakura-chain/src/transaction/tachyon_shielded.rs new file mode 100644 index 0000000000..fc034cc58e --- /dev/null +++ b/crates/zakura-chain/src/transaction/tachyon_shielded.rs @@ -0,0 +1,90 @@ +//! The Tachyon shielded data carried by V7 transactions. + +use zcash_tachyon::TachyonBundle; + +use crate::memory::{inline_size_bytes, vec_capacity_bytes, AttributedMemorySize}; + +/// A non-empty Tachyon bundle. +/// +/// `TachyonBundle::NoBundle` is represented by `None` on [`super::Transaction::V7`]. +#[derive(Clone, Debug)] +pub struct TachyonShieldedData(pub TachyonBundle); + +impl TachyonShieldedData { + /// Returns the bundle's actions. + pub fn actions(&self) -> &[zcash_tachyon::Action] { + match &self.0 { + TachyonBundle::NoBundle => &[], + TachyonBundle::Proven(bundle) => &bundle.actions, + TachyonBundle::Adjunct(bundle) => &bundle.actions, + } + } + + /// Returns the bundle's value balance. + pub fn value_balance(&self) -> zcash_tachyon::value::Balance { + match &self.0 { + TachyonBundle::NoBundle => zcash_tachyon::value::Balance::ZERO, + TachyonBundle::Proven(bundle) => bundle.value_balance, + TachyonBundle::Adjunct(bundle) => bundle.value_balance, + } + } +} + +impl From for TachyonShieldedData { + fn from(bundle: TachyonBundle) -> Self { + Self(bundle) + } +} + +impl PartialEq for TachyonShieldedData { + fn eq(&self, other: &Self) -> bool { + let mut self_bytes = Vec::new(); + let mut other_bytes = Vec::new(); + self.0 + .write(&mut self_bytes) + .expect("serializing a Tachyon bundle into a Vec is infallible"); + other + .0 + .write(&mut other_bytes) + .expect("serializing a Tachyon bundle into a Vec is infallible"); + self_bytes == other_bytes + } +} + +impl Eq for TachyonShieldedData {} + +impl AttributedMemorySize for TachyonShieldedData { + fn attributed_memory_size_bytes(&self) -> u64 { + let bundle = match &self.0 { + TachyonBundle::NoBundle => return 0, + TachyonBundle::Proven(bundle) => bundle, + TachyonBundle::Adjunct(bundle) => { + return vec_capacity_bytes(&bundle.actions) + .saturating_add(vec_capacity_bytes(&bundle.memo)); + } + }; + + vec_capacity_bytes(&bundle.actions) + .saturating_add(vec_capacity_bytes(&bundle.memo)) + .saturating_add( + u64::try_from(bundle.stamp.tachygrams.len()) + .unwrap_or(u64::MAX) + .saturating_mul(inline_size_bytes::()), + ) + .saturating_add( + u64::try_from(std::mem::size_of_val(bundle.stamp.proof.as_ref())) + .unwrap_or(u64::MAX), + ) + } +} + +#[cfg(any(test, feature = "proptest-impl"))] +impl serde::Serialize for TachyonShieldedData { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::Error as _; + + let mut bytes = Vec::new(); + self.0.write(&mut bytes).map_err(S::Error::custom)?; + serde::Serialize::serialize(&bytes, serializer) + } +} diff --git a/crates/zakura-chain/src/transaction/tests.rs b/crates/zakura-chain/src/transaction/tests.rs index ccb1591e09..83d84a24d8 100644 --- a/crates/zakura-chain/src/transaction/tests.rs +++ b/crates/zakura-chain/src/transaction/tests.rs @@ -3,4 +3,6 @@ mod ironwood_v6_tx_hash; mod preallocate; mod prop; +#[cfg(zcash_unstable = "nutachyon")] +mod v7_vectors; mod vectors; diff --git a/crates/zakura-chain/src/transaction/tests/v7_vectors.rs b/crates/zakura-chain/src/transaction/tests/v7_vectors.rs new file mode 100644 index 0000000000..82a4e5f2a0 --- /dev/null +++ b/crates/zakura-chain/src/transaction/tests/v7_vectors.rs @@ -0,0 +1,255 @@ +//! V7 transaction fixtures and cross-implementation serialization vectors. + +use group::{ + ff::{FromUniformBytes, PrimeField}, + CurveAffine, GroupEncoding, +}; +use halo2::pasta::pallas; +use reddsa::{orchard::SpendAuth, SigningKey, VerificationKey}; +use zcash_tachyon::{ + Action, Anchor, Bundle, PointerStamp, ProofStamp, Tachygram, TachygramSetPoly, TachyonBundle, +}; + +use crate::{ + block::Height, + parameters::{NetworkUpgrade, TX_V7_VERSION_GROUP_ID}, + serialization::{SerializationError, ZcashDeserialize, ZcashSerialize}, + transaction::{LockTime, TachyonShieldedData, Transaction}, +}; + +fn action_with_seed(seed: u8, signature_byte: u8) -> Action { + let signing_key = SigningKey::::try_from( + pallas::Scalar::from_uniform_bytes(&[seed; 64]).to_repr(), + ) + .expect("the reduced scalar is a valid signing key"); + let verification_key = VerificationKey::::from(&signing_key); + let verification_key = Option::::from(pallas::Affine::from_bytes( + &<[u8; 32]>::from(verification_key), + )) + .expect("the verification key is a valid curve point"); + + Action { + cv: zcash_tachyon::value::Commitment::from(pallas::Affine::generator()), + rk: zcash_tachyon::keys::public::ActionVerificationKey::try_from(verification_key) + .expect("a non-identity verification key is valid"), + sig: zcash_tachyon::action::Signature::read(&[signature_byte; 64][..]) + .expect("test signature bytes are canonical"), + } +} + +fn default_anchor() -> Anchor { + Anchor::read(&[0; 64][..]).expect("64 zero bytes encode the default Tachyon anchor") +} + +fn tachygram(seed: u8) -> Tachygram { + Tachygram::from(pallas::Base::from_uniform_bytes(&[seed; 64])) +} + +fn transaction_with_tachyon_bundle(bundle: TachyonBundle) -> Transaction { + let tachyon_shielded_data = (!bundle.is_no_bundle()).then_some(TachyonShieldedData(bundle)); + + Transaction::V7 { + network_upgrade: NetworkUpgrade::NuTachyon, + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + inputs: Vec::new(), + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + ironwood_shielded_data: None, + tachyon_shielded_data, + } +} + +fn empty_transaction() -> Transaction { + transaction_with_tachyon_bundle(TachyonBundle::NoBundle) +} + +fn adjunct_transaction() -> Transaction { + transaction_with_tachyon_bundle(TachyonBundle::Adjunct(Bundle { + actions: vec![action_with_seed(0x42, 0x01)], + value_balance: zcash_tachyon::value::Balance::ZERO, + binding_sig: zcash_tachyon::bundle::Signature::read(&[0x02; 64][..]) + .expect("test signature bytes are canonical"), + memo: Vec::new(), + stamp: PointerStamp::try_from([0xEE; 64]) + .expect("a 64-byte witnessed transaction ID is valid"), + })) +} + +fn proven_transaction() -> Transaction { + let tachygram = tachygram(0xAA); + + transaction_with_tachyon_bundle(TachyonBundle::Proven(Bundle { + actions: vec![action_with_seed(0x42, 0x01)], + value_balance: zcash_tachyon::value::Balance::try_from(100i64) + .expect("100 is a valid Tachyon value balance"), + binding_sig: zcash_tachyon::bundle::Signature::read(&[0x02; 64][..]) + .expect("test signature bytes are canonical"), + memo: Vec::new(), + stamp: ProofStamp { + coverage: [0; 32], + tachygram_set: [tachygram] + .into_iter() + .collect::() + .commit(), + tachygrams: [tachygram].into_iter().collect(), + anchor: default_anchor(), + proof: Box::new(ragu::Proof::trivial()), + }, + })) +} + +fn multi_action_proven_transaction() -> Transaction { + let tachygrams = [tachygram(0xAA), tachygram(0xCC), tachygram(0xDD)]; + + transaction_with_tachyon_bundle(TachyonBundle::Proven(Bundle { + actions: vec![action_with_seed(0x42, 0x01), action_with_seed(0x43, 0x03)], + value_balance: zcash_tachyon::value::Balance::try_from(300i64) + .expect("300 is a valid Tachyon value balance"), + binding_sig: zcash_tachyon::bundle::Signature::read(&[0x02; 64][..]) + .expect("test signature bytes are canonical"), + memo: Vec::new(), + stamp: ProofStamp { + coverage: [0; 32], + tachygram_set: tachygrams + .into_iter() + .collect::() + .commit(), + tachygrams: tachygrams.into_iter().collect(), + anchor: default_anchor(), + proof: Box::new(ragu::Proof::trivial()), + }, + })) +} + +fn fixtures() -> [(&'static str, Transaction); 4] { + [ + ("V7_EMPTY", empty_transaction()), + ("V7_TACHYON_ADJUNCT", adjunct_transaction()), + ("V7_TACHYON_PROVEN", proven_transaction()), + ( + "V7_TACHYON_MULTI_ACTION_PROVEN", + multi_action_proven_transaction(), + ), + ] +} + +#[test] +fn v7_is_nu_tachyon_gated_and_matches_zakura_primitives() { + let _init_guard = zakura_test::init(); + + assert_eq!( + TX_V7_VERSION_GROUP_ID, + zcash_protocol::constants::V7_VERSION_GROUP_ID, + ); + assert_eq!( + TX_V7_VERSION_GROUP_ID, + zcash_primitives::transaction::TxVersion::V7.version_group_id(), + ); + + let pre_nu_tachyon = Transaction::V7 { + network_upgrade: NetworkUpgrade::Nu6_3, + lock_time: LockTime::unlocked(), + expiry_height: Height(0), + inputs: Vec::new(), + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + ironwood_shielded_data: None, + tachyon_shielded_data: None, + }; + pre_nu_tachyon + .zcash_serialize_to_vec() + .expect_err("V7 must not serialize before NuTachyon"); + + let transaction = empty_transaction(); + let bytes = transaction + .zcash_serialize_to_vec() + .expect("an empty NuTachyon V7 transaction has a valid wire encoding"); + assert_eq!(&bytes[0..4], &[0x07, 0x00, 0x00, 0x80]); + assert_eq!(&bytes[4..8], &TX_V7_VERSION_GROUP_ID.to_le_bytes()); + assert_eq!(&bytes[8..12], &0xffff_fffcu32.to_le_bytes()); + assert_eq!(transaction.version_group_id(), Some(TX_V7_VERSION_GROUP_ID)); + + let decoded = Transaction::zcash_deserialize(&bytes[..]) + .expect("Zakura must deserialize its NuTachyon V7 encoding"); + assert_eq!(decoded, transaction); + let zakura_primitives_transaction = transaction + .to_librustzcash(NetworkUpgrade::NuTachyon) + .expect("zakura-primitives must accept Zakura's NuTachyon V7 encoding"); + assert_eq!( + transaction.hash().0, + *zakura_primitives_transaction.txid().as_ref() + ); + let zakura_primitives_auth_digest: [u8; 32] = zakura_primitives_transaction + .auth_commitment() + .as_ref() + .try_into() + .expect("the authorizing-data digest is 32 bytes"); + assert_eq!( + transaction + .auth_digest() + .expect("V7 transactions have an authorizing-data digest") + .0, + zakura_primitives_auth_digest, + ); + + let mut pre_nu_tachyon_bytes = bytes; + let nu6_3_branch_id = u32::from( + NetworkUpgrade::Nu6_3 + .branch_id() + .expect("NU6.3 has a branch ID"), + ); + pre_nu_tachyon_bytes[8..12].copy_from_slice(&nu6_3_branch_id.to_le_bytes()); + let error = Transaction::zcash_deserialize(&pre_nu_tachyon_bytes[..]) + .expect_err("V7 must not deserialize with a pre-NuTachyon branch ID"); + assert!(matches!( + error, + SerializationError::Parse(message) if message.contains("NuTachyon") + )); +} + +#[test] +fn v7_tachyon_fixtures_round_trip() { + let _init_guard = zakura_test::init(); + + for (name, transaction) in fixtures() { + let bytes = transaction + .zcash_serialize_to_vec() + .unwrap_or_else(|error| panic!("{name} serialization failed: {error}")); + let decoded = Transaction::zcash_deserialize(&bytes[..]) + .unwrap_or_else(|error| panic!("{name} deserialization failed: {error}")); + let reencoded = decoded + .zcash_serialize_to_vec() + .unwrap_or_else(|error| panic!("{name} reserialization failed: {error}")); + + assert_eq!(decoded, transaction, "{name} round-trip mismatch"); + assert_eq!(reencoded, bytes, "{name} reserialization differs"); + assert_eq!(decoded.hash(), transaction.hash(), "{name} txid differs"); + assert_eq!( + decoded.auth_digest(), + transaction.auth_digest(), + "{name} authorization digest differs" + ); + } +} + +/// Prints the canonical encodings consumed by `zakura-primitives` tests. +#[test] +#[ignore = "prints V7 serialization vectors for zakura-primitives"] +#[allow(clippy::print_stdout)] +fn generate_v7_tachyon_serialization_vectors() { + let _init_guard = zakura_test::init(); + + for (name, transaction) in fixtures() { + let bytes = transaction + .zcash_serialize_to_vec() + .unwrap_or_else(|error| panic!("{name} serialization failed: {error}")); + let decoded = Transaction::zcash_deserialize(&bytes[..]) + .unwrap_or_else(|error| panic!("{name} deserialization failed: {error}")); + + assert_eq!(decoded, transaction, "{name} round-trip mismatch"); + println!("{name}={}", hex::encode(bytes)); + } +} diff --git a/crates/zakura-chain/src/transaction/tests/vectors.rs b/crates/zakura-chain/src/transaction/tests/vectors.rs index 53e6407a2b..4834df0304 100644 --- a/crates/zakura-chain/src/transaction/tests/vectors.rs +++ b/crates/zakura-chain/src/transaction/tests/vectors.rs @@ -1745,6 +1745,38 @@ fn binding_signatures() { ) .expect("a valid redjubjub::VerificationKey"); + bvk.verify(sighash.as_ref(), &sapling_shielded_data.binding_sig) + .expect("verification passes"); + } + } + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data, + .. + } => { + if let Some(sapling_shielded_data) = sapling_shielded_data { + // V6 txs have the outputs spent by their transparent inputs hashed into + // their SIGHASH, so we need to exclude txs with transparent inputs. + // + // References: + // + // + // + if tx.has_transparent_inputs() { + continue; + } + + let sighash = tx + .sighash(nu, HashType::ALL, Arc::new(Vec::new()), None) + .expect("network upgrade is valid for tx"); + + let bvk = redjubjub::VerificationKey::try_from( + sapling_shielded_data + .binding_verification_key() + .expect("test transaction has valid value commitments"), + ) + .expect("a valid redjubjub::VerificationKey"); + bvk.verify(sighash.as_ref(), &sapling_shielded_data.binding_sig) .expect("verification passes"); } diff --git a/crates/zakura-chain/src/transaction/txid.rs b/crates/zakura-chain/src/transaction/txid.rs index 1b7033dae4..ad1b3ffbe5 100644 --- a/crates/zakura-chain/src/transaction/txid.rs +++ b/crates/zakura-chain/src/transaction/txid.rs @@ -6,7 +6,7 @@ use crate::serialization::{sha256d, ZcashSerialize}; /// A Transaction ID builder. It computes the transaction ID by hashing /// different parts of the transaction, depending on the transaction version. -/// For V5 and V6 transactions, it follows [ZIP-244] and [ZIP-225]. +/// For V5 and later transactions, it follows [ZIP-244] and [ZIP-225]. /// /// [ZIP-244]: https://zips.z.cash/zip-0244 /// [ZIP-225]: https://zips.z.cash/zip-0225 @@ -29,6 +29,8 @@ impl<'a> TxIdBuilder<'a> { | Transaction::V4 { .. } => self.txid_v1_to_v4(), Transaction::V5 { .. } => self.txid_v5(), Transaction::V6 { .. } => self.txid_v6(), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => self.txid_v6(), } } @@ -40,7 +42,7 @@ impl<'a> TxIdBuilder<'a> { Some(Hash(hash_writer.finish())) } - /// Compute the Transaction ID for a V5 or V6 transaction. + /// Compute the Transaction ID for a V5 or later transaction. /// 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 { @@ -56,7 +58,7 @@ impl<'a> TxIdBuilder<'a> { Some(Hash(*self.trans.to_librustzcash(nu).ok()?.txid().as_ref())) } - /// Passthrough to the ZIP-244 txid path for V6 transactions. + /// Passthrough to the ZIP-244 txid path for V6 and V7 transactions. fn txid_v6(self) -> Option { self.txid_v5() } diff --git a/crates/zakura-chain/src/transaction/unmined.rs b/crates/zakura-chain/src/transaction/unmined.rs index a899963484..e3acc4cc0f 100644 --- a/crates/zakura-chain/src/transaction/unmined.rs +++ b/crates/zakura-chain/src/transaction/unmined.rs @@ -1,8 +1,8 @@ //! Unmined Zcash transaction identifiers and transactions. //! -//! Transaction versions 5 and 6 are uniquely identified by [`WtxId`] when +//! Transaction versions 5 and later are uniquely identified by [`WtxId`] when //! unmined, and [`struct@Hash`] in the blockchain. The effects of a v5 or -//! v6 transaction (spends and outputs) are uniquely identified by the same +//! v5+ transaction (spends and outputs) are uniquely identified by the same //! [`struct@Hash`] in both cases. //! //! Transaction versions 1-4 are uniquely identified by legacy @@ -98,8 +98,8 @@ pub enum UnminedTxId { /// A witnessed unmined transaction identifier. /// - /// Used to uniquely identify unmined version 5 and 6 transactions. - /// (After v5 and v6 transactions are mined, they can be uniquely identified + /// Used to uniquely identify unmined version 5+ transactions. + /// (After v5+ transactions are mined, they can be uniquely identified /// using only the [`struct@Hash`] in their `WtxId.id`.) /// /// For more details, see [`WtxId`]. @@ -142,6 +142,8 @@ impl From<&Transaction> for UnminedTxId { V1 { .. } | V2 { .. } | V3 { .. } | V4 { .. } => Legacy(transaction.into()), V5 { .. } => Witnessed(transaction.into()), V6 { .. } => Witnessed(transaction.into()), + #[cfg(zcash_unstable = "nutachyon")] + V7 { .. } => Witnessed(transaction.into()), } } } diff --git a/crates/zakura-chain/src/value_balance.rs b/crates/zakura-chain/src/value_balance.rs index b01e8f6449..c82b32af3b 100644 --- a/crates/zakura-chain/src/value_balance.rs +++ b/crates/zakura-chain/src/value_balance.rs @@ -18,6 +18,11 @@ mod tests; use ValueBalanceError::*; +#[cfg(not(zcash_unstable = "nutachyon"))] +const VALUE_BALANCE_BYTES: usize = 48; +#[cfg(zcash_unstable = "nutachyon")] +const VALUE_BALANCE_BYTES: usize = 56; + /// A balance in each chain value pool or transaction value pool. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct ValueBalance { @@ -27,6 +32,8 @@ pub struct ValueBalance { orchard: Amount, deferred: Amount, ironwood: Amount, + #[cfg(zcash_unstable = "nutachyon")] + tachyon: Amount, } impl ValueBalance @@ -73,6 +80,15 @@ where } } + /// Creates a [`ValueBalance`] from the given Tachyon amount. + #[cfg(zcash_unstable = "nutachyon")] + pub fn from_tachyon_amount(tachyon_amount: Amount) -> Self { + ValueBalance { + tachyon: tachyon_amount, + ..ValueBalance::zero() + } + } + /// Get the transparent amount from the [`ValueBalance`]. pub fn transparent_amount(&self) -> Amount { self.transparent @@ -147,6 +163,19 @@ where self } + /// Get the Tachyon amount from the [`ValueBalance`]. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_amount(&self) -> Amount { + self.tachyon + } + + /// Insert a Tachyon value balance without changing the other pools. + #[cfg(zcash_unstable = "nutachyon")] + pub fn set_tachyon_value_balance(&mut self, tachyon_value_balance: ValueBalance) -> &Self { + self.tachyon = tachyon_value_balance.tachyon; + self + } + /// Creates a [`ValueBalance`] where all the pools are zero. pub fn zero() -> Self { let zero = Amount::zero(); @@ -157,6 +186,8 @@ where orchard: zero, deferred: zero, ironwood: zero, + #[cfg(zcash_unstable = "nutachyon")] + tachyon: zero, } } @@ -173,6 +204,8 @@ where orchard: self.orchard.constrain().map_err(Orchard)?, deferred: self.deferred.constrain().map_err(Deferred)?, ironwood: self.ironwood.constrain().map_err(Ironwood)?, + #[cfg(zcash_unstable = "nutachyon")] + tachyon: self.tachyon.constrain().map_err(Tachyon)?, }) } } @@ -199,8 +232,12 @@ impl ValueBalance { // like the orchard bundle; it is zero for transactions without an ironwood bundle. // // This will error if the remaining value in the transaction value pool is negative. - (self.transparent + self.sprout + self.sapling + self.orchard + self.ironwood)? - .constrain::() + let remaining = + self.transparent + self.sprout + self.sapling + self.orchard + self.ironwood; + #[cfg(zcash_unstable = "nutachyon")] + let remaining = remaining? + self.tachyon; + + remaining?.constrain::() } } @@ -350,10 +387,9 @@ impl ValueBalance { /// To byte array /// - /// The `ironwood` pool (NU6.3 onward) is appended after `deferred`, so that records written by - /// earlier Zebra versions (32 bytes without `deferred`, or 40 bytes with it) remain parsable by + /// New pools are appended in activation order, so older records remain parsable by /// [`Self::from_bytes`]. - pub fn to_bytes(self) -> [u8; 48] { + pub fn to_bytes(self) -> [u8; VALUE_BALANCE_BYTES] { match [ self.transparent.to_bytes(), self.sprout.to_bytes(), @@ -361,30 +397,33 @@ impl ValueBalance { self.orchard.to_bytes(), self.deferred.to_bytes(), self.ironwood.to_bytes(), + #[cfg(zcash_unstable = "nutachyon")] + self.tachyon.to_bytes(), ] .concat() .try_into() { Ok(bytes) => bytes, _ => unreachable!( - "six [u8; 8] should always concat with no error into a single [u8; 48]" + "the value balance fields should concat into the configured fixed-width array" ), } } /// From byte array /// - /// Accepts 32-byte (pre-`deferred`), 40-byte (pre-`ironwood`), and 48-byte records; missing - /// trailing pools default to zero. + /// Accepts 32-byte (pre-`deferred`), 40-byte (pre-`ironwood`), 48-byte (pre-`tachyon`), + /// and 56-byte records; missing trailing pools default to zero. #[allow(clippy::unwrap_in_result)] pub fn from_bytes(bytes: &[u8]) -> Result, ValueBalanceError> { let bytes_length = bytes.len(); // Return an error early if bytes don't have the right length instead of panicking later. - match bytes_length { - 32 | 40 | 48 => {} - _ => return Err(Unparsable), - }; + let valid_length = matches!(bytes_length, 32 | 40 | 48) + || cfg!(zcash_unstable = "nutachyon") && bytes_length == 56; + if !valid_length { + return Err(Unparsable); + } let transparent = Amount::from_bytes( bytes[0..8] @@ -416,7 +455,7 @@ impl ValueBalance { let deferred = match bytes_length { 32 => Amount::zero(), - 40 | 48 => Amount::from_bytes( + 40 | 48 | 56 => Amount::from_bytes( bytes[32..40] .try_into() .expect("deferred amount should be parsable"), @@ -427,7 +466,7 @@ impl ValueBalance { let ironwood = match bytes_length { 32 | 40 => Amount::zero(), - 48 => Amount::from_bytes( + 48 | 56 => Amount::from_bytes( bytes[40..48] .try_into() .expect("ironwood amount should be parsable"), @@ -436,6 +475,18 @@ impl ValueBalance { _ => return Err(Unparsable), }; + #[cfg(zcash_unstable = "nutachyon")] + let tachyon = match bytes_length { + 32 | 40 | 48 => Amount::zero(), + 56 => Amount::from_bytes( + bytes[48..56] + .try_into() + .expect("tachyon amount should be parsable"), + ) + .map_err(Tachyon)?, + _ => return Err(Unparsable), + }; + Ok(ValueBalance { transparent, sprout, @@ -443,6 +494,8 @@ impl ValueBalance { orchard, deferred, ironwood, + #[cfg(zcash_unstable = "nutachyon")] + tachyon, }) } } @@ -468,6 +521,10 @@ pub enum ValueBalanceError { /// ironwood amount error {0} Ironwood(amount::Error), + /// Tachyon amount error {0} + #[cfg(zcash_unstable = "nutachyon")] + Tachyon(amount::Error), + /// ValueBalance is unparsable Unparsable, } @@ -481,6 +538,8 @@ impl fmt::Display for ValueBalanceError { Orchard(e) => format!("orchard amount err: {e}"), Deferred(e) => format!("deferred amount err: {e}"), Ironwood(e) => format!("ironwood amount err: {e}"), + #[cfg(zcash_unstable = "nutachyon")] + Tachyon(e) => format!("tachyon amount err: {e}"), Unparsable => "value balance is unparsable".to_string(), }) } @@ -499,6 +558,8 @@ where orchard: (self.orchard + rhs.orchard).map_err(Orchard)?, deferred: (self.deferred + rhs.deferred).map_err(Deferred)?, ironwood: (self.ironwood + rhs.ironwood).map_err(Ironwood)?, + #[cfg(zcash_unstable = "nutachyon")] + tachyon: (self.tachyon + rhs.tachyon).map_err(Tachyon)?, }) } } @@ -549,6 +610,8 @@ where orchard: (self.orchard - rhs.orchard).map_err(Orchard)?, deferred: (self.deferred - rhs.deferred).map_err(Deferred)?, ironwood: (self.ironwood - rhs.ironwood).map_err(Ironwood)?, + #[cfg(zcash_unstable = "nutachyon")] + tachyon: (self.tachyon - rhs.tachyon).map_err(Tachyon)?, }) } } @@ -619,6 +682,8 @@ where orchard: self.orchard.neg(), deferred: self.deferred.neg(), ironwood: self.ironwood.neg(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon: self.tachyon.neg(), } } } diff --git a/crates/zakura-chain/src/value_balance/arbitrary.rs b/crates/zakura-chain/src/value_balance/arbitrary.rs index 9cbdbeb73a..14cf1c1550 100644 --- a/crates/zakura-chain/src/value_balance/arbitrary.rs +++ b/crates/zakura-chain/src/value_balance/arbitrary.rs @@ -1,6 +1,38 @@ use crate::{amount::*, value_balance::*}; use proptest::prelude::*; +#[cfg(zcash_unstable = "nutachyon")] +impl Arbitrary for ValueBalance { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::>(), + any::>(), + any::>(), + any::>(), + any::>(), + any::>(), + any::>(), + ) + .prop_map( + |(transparent, sprout, sapling, orchard, deferred, ironwood, tachyon)| Self { + transparent, + sprout, + sapling, + orchard, + deferred, + ironwood, + tachyon, + }, + ) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +#[cfg(not(zcash_unstable = "nutachyon"))] impl Arbitrary for ValueBalance { type Parameters = (); @@ -29,6 +61,38 @@ impl Arbitrary for ValueBalance { type Strategy = BoxedStrategy; } +#[cfg(zcash_unstable = "nutachyon")] +impl Arbitrary for ValueBalance { + type Parameters = (); + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::>(), + any::>(), + any::>(), + any::>(), + any::>(), + any::>(), + any::>(), + ) + .prop_map( + |(transparent, sprout, sapling, orchard, deferred, ironwood, tachyon)| Self { + transparent, + sprout, + sapling, + orchard, + deferred, + ironwood, + tachyon, + }, + ) + .boxed() + } + + type Strategy = BoxedStrategy; +} + +#[cfg(not(zcash_unstable = "nutachyon"))] impl Arbitrary for ValueBalance { type Parameters = (); diff --git a/crates/zakura-chain/src/value_balance/tests/prop.rs b/crates/zakura-chain/src/value_balance/tests/prop.rs index 162721e023..b703daa1d9 100644 --- a/crates/zakura-chain/src/value_balance/tests/prop.rs +++ b/crates/zakura-chain/src/value_balance/tests/prop.rs @@ -6,6 +6,7 @@ use crate::{amount::*, value_balance::*}; proptest! { #[test] + #[cfg(zcash_unstable = "nutachyon")] fn value_blance_add( value_balance1 in any::>(), value_balance2 in any::>()) @@ -18,9 +19,10 @@ proptest! { let orchard = value_balance1.orchard + value_balance2.orchard; let deferred = value_balance1.deferred + value_balance2.deferred; let ironwood = value_balance1.ironwood + value_balance2.ironwood; + let tachyon = value_balance1.tachyon + value_balance2.tachyon; - match (transparent, sprout, sapling, orchard, deferred, ironwood) { - (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred), Ok(ironwood)) => prop_assert_eq!( + match (transparent, sprout, sapling, orchard, deferred, ironwood, tachyon) { + (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred), Ok(ironwood), Ok(tachyon)) => prop_assert_eq!( value_balance1 + value_balance2, Ok(ValueBalance { transparent, @@ -28,7 +30,8 @@ proptest! { sapling, orchard, deferred, - ironwood + ironwood, + tachyon }) ), _ => prop_assert!( @@ -39,12 +42,14 @@ proptest! { | ValueBalanceError::Sapling(_) | ValueBalanceError::Orchard(_) | ValueBalanceError::Deferred(_) - | ValueBalanceError::Ironwood(_)) + | ValueBalanceError::Ironwood(_) + | ValueBalanceError::Tachyon(_)) ) ), } } #[test] + #[cfg(zcash_unstable = "nutachyon")] fn value_balance_sub( value_balance1 in any::>(), value_balance2 in any::>()) @@ -57,9 +62,10 @@ proptest! { let orchard = value_balance1.orchard - value_balance2.orchard; let deferred = value_balance1.deferred - value_balance2.deferred; let ironwood = value_balance1.ironwood - value_balance2.ironwood; + let tachyon = value_balance1.tachyon - value_balance2.tachyon; - match (transparent, sprout, sapling, orchard, deferred, ironwood) { - (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred), Ok(ironwood)) => prop_assert_eq!( + match (transparent, sprout, sapling, orchard, deferred, ironwood, tachyon) { + (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred), Ok(ironwood), Ok(tachyon)) => prop_assert_eq!( value_balance1 - value_balance2, Ok(ValueBalance { transparent, @@ -67,7 +73,8 @@ proptest! { sapling, orchard, deferred, - ironwood + ironwood, + tachyon }) ), _ => prop_assert!(matches!( @@ -77,12 +84,14 @@ proptest! { | ValueBalanceError::Sapling(_) | ValueBalanceError::Orchard(_) | ValueBalanceError::Deferred(_) - | ValueBalanceError::Ironwood(_)) + | ValueBalanceError::Ironwood(_) + | ValueBalanceError::Tachyon(_)) )), } } #[test] + #[cfg(zcash_unstable = "nutachyon")] fn value_balance_sum( value_balance1 in any::>(), value_balance2 in any::>(), @@ -97,9 +106,10 @@ proptest! { let orchard = value_balance1.orchard + value_balance2.orchard; let deferred = value_balance1.deferred + value_balance2.deferred; let ironwood = value_balance1.ironwood + value_balance2.ironwood; + let tachyon = value_balance1.tachyon + value_balance2.tachyon; - match (transparent, sprout, sapling, orchard, deferred, ironwood) { - (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred), Ok(ironwood)) => prop_assert_eq!( + match (transparent, sprout, sapling, orchard, deferred, ironwood, tachyon) { + (Ok(transparent), Ok(sprout), Ok(sapling), Ok(orchard), Ok(deferred), Ok(ironwood), Ok(tachyon)) => prop_assert_eq!( collection.iter().sum::, ValueBalanceError>>(), Ok(ValueBalance { transparent, @@ -107,7 +117,8 @@ proptest! { sapling, orchard, deferred, - ironwood + ironwood, + tachyon }) ), _ => prop_assert!(matches!( @@ -117,7 +128,8 @@ proptest! { | ValueBalanceError::Sapling(_) | ValueBalanceError::Orchard(_) | ValueBalanceError::Deferred(_) - | ValueBalanceError::Ironwood(_)) + | ValueBalanceError::Ironwood(_) + | ValueBalanceError::Tachyon(_)) )) } } @@ -132,7 +144,8 @@ proptest! { } #[test] - fn value_balance_deserialization(bytes in any::<[u8; 48]>()) { + #[cfg(zcash_unstable = "nutachyon")] + fn value_balance_deserialization(bytes in any::<[u8; 56]>()) { let _init_guard = zakura_test::init(); if let Ok(deserialized) = ValueBalance::::from_bytes(&bytes) { @@ -140,7 +153,7 @@ proptest! { } } - /// The legacy version of [`ValueBalance`] had 32 bytes compared to the current 48 bytes, + /// The legacy version of [`ValueBalance`] had 32 bytes compared to the current 56 bytes, /// but it's possible to correctly instantiate the current version of [`ValueBalance`] from /// the legacy format, so we test if Zebra can still deserialize the legacy format. #[test] @@ -149,7 +162,7 @@ proptest! { if let Ok(deserialized) = ValueBalance::::from_bytes(&bytes) { let deserialized = deserialized.to_bytes(); - let mut extended_bytes = [0u8; 48]; + let mut extended_bytes = [0u8; VALUE_BALANCE_BYTES]; extended_bytes[..32].copy_from_slice(&bytes); prop_assert_eq!(extended_bytes, deserialized); } @@ -165,10 +178,24 @@ proptest! { if let Ok(deserialized) = ValueBalance::::from_bytes(&bytes) { let deserialized = deserialized.to_bytes(); - let mut extended_bytes = [0u8; 48]; + let mut extended_bytes = [0u8; VALUE_BALANCE_BYTES]; extended_bytes[..40].copy_from_slice(&bytes); prop_assert_eq!(extended_bytes, deserialized); } } + /// The pre-Tachyon value balance was a 48-byte prefix of the current format. + #[test] + #[cfg(zcash_unstable = "nutachyon")] + fn pre_tachyon_value_balance_deserialization(bytes in any::<[u8; 48]>()) { + let _init_guard = zakura_test::init(); + + if let Ok(deserialized) = ValueBalance::::from_bytes(&bytes) { + let deserialized = deserialized.to_bytes(); + let mut extended_bytes = [0u8; 56]; + extended_bytes[..48].copy_from_slice(&bytes); + prop_assert_eq!(extended_bytes, deserialized); + } + } + } diff --git a/crates/zakura-consensus/Cargo.toml b/crates/zakura-consensus/Cargo.toml index b79c398e01..a36b20df8a 100644 --- a/crates/zakura-consensus/Cargo.toml +++ b/crates/zakura-consensus/Cargo.toml @@ -82,6 +82,12 @@ howudoin = { workspace = true, optional = true } proptest = { workspace = true, optional = true } proptest-derive = { workspace = true, optional = true } +[target.'cfg(zcash_unstable = "nutachyon")'.dependencies] +zcash_tachyon.workspace = true + +[target.'cfg(zcash_unstable = "nutachyon")'.dev-dependencies] +ragu = { workspace = true } + [dev-dependencies] color-eyre = { workspace = true } diff --git a/crates/zakura-consensus/src/block.rs b/crates/zakura-consensus/src/block.rs index 565db91780..1b1a576edc 100644 --- a/crates/zakura-consensus/src/block.rs +++ b/crates/zakura-consensus/src/block.rs @@ -36,6 +36,8 @@ use crate::{error::*, primitives, transaction as tx, BoxError}; pub mod check; pub mod request; pub mod subsidy; +#[cfg(zcash_unstable = "nutachyon")] +pub(crate) mod tachyon; pub use request::Request; @@ -125,6 +127,22 @@ impl VerifyBlockError { consensus("block.too_many_transparent_signature_operations") } BlockError::SummingMinerFees { .. } => consensus("block.summing_miner_fees"), + #[cfg(zcash_unstable = "nutachyon")] + BlockError::DuplicateTachygram => consensus("block.duplicate_tachygram"), + #[cfg(zcash_unstable = "nutachyon")] + BlockError::TachyonAggregateNotFound => { + consensus("block.tachyon_aggregate_not_found") + } + #[cfg(zcash_unstable = "nutachyon")] + BlockError::TachyonCoverageMismatch => consensus("block.tachyon_coverage_mismatch"), + #[cfg(zcash_unstable = "nutachyon")] + BlockError::TachyonDuplicateAction => consensus("block.tachyon_duplicate_action"), + #[cfg(zcash_unstable = "nutachyon")] + BlockError::TachyonTachygramArityMismatch => { + consensus("block.tachyon_tachygram_arity_mismatch") + } + #[cfg(zcash_unstable = "nutachyon")] + BlockError::TachyonProofInvalid(_) => consensus("block.tachyon_proof_invalid"), BlockError::InvalidHeaderEncoding(_) | BlockError::MissingHeight(_) | BlockError::MaxHeight(_, _, _) @@ -317,6 +335,12 @@ where check::merkle_root_validity(&network, &block, &transaction_hashes)?; + #[cfg(zcash_unstable = "nutachyon")] + let mut tachyon_checks = tachyon::coherence(&block)? + .into_iter() + .map(primitives::tachyon::verify_proof_stamp) + .collect::>(); + // Since errors cause an early exit, try to do the // quick checks first. @@ -403,6 +427,11 @@ where // Check the summed block totals + #[cfg(zcash_unstable = "nutachyon")] + while let Some(result) = tachyon_checks.next().await { + result?; + } + if sigops > MAX_BLOCK_SIGOPS { Err(BlockError::TooManyTransparentSignatureOperations { height, diff --git a/crates/zakura-consensus/src/block/tachyon.rs b/crates/zakura-consensus/src/block/tachyon.rs new file mode 100644 index 0000000000..b6c84021c5 --- /dev/null +++ b/crates/zakura-consensus/src/block/tachyon.rs @@ -0,0 +1,84 @@ +//! Block-level Tachyon consensus rules. + +use std::collections::{BTreeSet, HashMap}; + +use zakura_chain::{block::Block, transaction::WtxId}; +use zcash_tachyon::{ + stamp::StampState as _, Bundle, PointerStamp, ProofStamp, Tachygram, TachyonBundle, + VerifyCoverageError, +}; + +use crate::error::BlockError; + +#[cfg(test)] +mod tests; + +/// A proof-stamped bundle and the pointer-stamped bundles it covers. +#[derive(Debug)] +pub(crate) struct AggregateCoverage { + pub bundle: Bundle, + pub adjuncts: Vec>, +} + +/// Checks block-wide Tachygram distinctness and aggregate coverage coherence. +pub(crate) fn coherence(block: &Block) -> Result, BlockError> { + let mut seen_tachygrams = BTreeSet::::new(); + let mut aggregates = Vec::::new(); + let mut aggregates_by_wtxid = HashMap::<[u8; 64], usize>::new(); + let mut adjuncts = Vec::<([u8; 64], Bundle)>::new(); + + for transaction in &block.transactions { + let Some(tachyon_shielded_data) = transaction.tachyon_shielded_data() else { + continue; + }; + + match &tachyon_shielded_data.0 { + TachyonBundle::NoBundle => {} + TachyonBundle::Proven(bundle) => { + for &tachygram in &bundle.stamp.tachygrams { + if !seen_tachygrams.insert(tachygram) { + return Err(BlockError::DuplicateTachygram); + } + } + + let wtxid = WtxId::from(transaction.as_ref()).as_bytes(); + aggregates_by_wtxid.insert(wtxid, aggregates.len()); + aggregates.push(AggregateCoverage { + bundle: bundle.clone(), + adjuncts: Vec::new(), + }); + } + TachyonBundle::Adjunct(bundle) => { + adjuncts.push((bundle.stamp.stamp_digest(), bundle.clone())); + } + } + } + + for (target_wtxid, bundle) in adjuncts { + let Some(&aggregate_index) = aggregates_by_wtxid.get(&target_wtxid) else { + return Err(BlockError::TachyonAggregateNotFound); + }; + aggregates[aggregate_index].adjuncts.push(bundle); + } + + for aggregate in &aggregates { + let adjunct_refs: Vec<_> = aggregate + .adjuncts + .iter() + .map(|adjunct| adjunct.as_dyn()) + .collect(); + + aggregate + .bundle + .verify_coverage(&adjunct_refs) + .map_err(|error| match error { + VerifyCoverageError::DuplicateActions => BlockError::TachyonDuplicateAction, + VerifyCoverageError::StampActionsMismatch => BlockError::TachyonCoverageMismatch, + VerifyCoverageError::TachygramArityMismatch => { + BlockError::TachyonTachygramArityMismatch + } + })?; + } + + Ok(aggregates) +} diff --git a/crates/zakura-consensus/src/block/tachyon/tests.rs b/crates/zakura-consensus/src/block/tachyon/tests.rs new file mode 100644 index 0000000000..e0d538094d --- /dev/null +++ b/crates/zakura-consensus/src/block/tachyon/tests.rs @@ -0,0 +1,368 @@ +//! Tests for the block-level tachyon consensus rules. + +use std::sync::Arc; + +use zakura_chain::{ + block::{Block, Height}, + parameters::NetworkUpgrade, + serialization::ZcashDeserialize, + transaction::{LockTime, Transaction, WtxId}, +}; + +use zcash_tachyon::{ + action, bundle, effect, + entropy::ActionEntropy, + keys::private, + note::{CommitmentTrapdoor, Note}, + nullifier, value, Anchor, Bundle, PointerStamp, ProofStamp, Tachygram, TachygramSetPoly, + TachyonBundle, +}; + +use crate::error::BlockError; + +use super::{coherence, AggregateCoverage}; + +/// Computes `hStampActionsTachyon` over descriptor bytes. +/// +/// This duplicates tachyon's private `blake2b::action_descriptor_digest` (BLAKE2b-256, +/// personalization `Tachyon-Actions`, over the sorted concatenated 64-byte descriptors). Tests +/// using it self-validate via [`Bundle::verify_coverage`], so drift from the tachyon crate fails +/// loudly instead of silently testing against the wrong digest. +fn action_descriptor_digest(descriptors: &[action::Descriptor]) -> [u8; 32] { + // `Vec<[u8; 64]>: FromIterator` is tachyon's public descriptor-bytes conversion. + let mut descriptor_bytes: Vec<[u8; 64]> = descriptors.iter().copied().collect(); + descriptor_bytes.sort_unstable(); + + let mut state = blake2b_simd::Params::new() + .hash_length(32) + .personal(b"Tachyon-Actions") + .to_state(); + for descriptor in &descriptor_bytes { + state.update(descriptor); + } + state + .finalize() + .as_bytes() + .try_into() + .expect("hash length is 32") +} + +/// A structurally valid action with a random `rk` and dummy signature: block-level coherence +/// never checks signatures. +fn dummy_action() -> zcash_tachyon::Action { + use halo2::pasta::group::CurveAffine; + + let mut rng = rand_10::rng(); + + let sk = private::SpendingKey::random(&mut rng); + let note = Note { + pk: sk.derive_payment_key(), + value: value::Positive::try_from(100u64).expect("valid value"), + psi: nullifier::Trapdoor::random(&mut rng), + rcm: CommitmentTrapdoor::random(&mut rng), + }; + let alpha = ActionEntropy::random(&mut rng).randomizer::(note.commitment()); + let rk = private::ActionSigningKey::new(&alpha).derive_action_public(); + + zcash_tachyon::Action { + cv: value::Commitment::from(halo2::pasta::pallas::Affine::generator()), + rk, + sig: action::Signature::read(&[0x01u8; 64][..]).expect("64 bytes"), + } +} + +/// The all-zero anchor. +fn zero_anchor() -> Anchor { + Anchor::read(&[0u8; 32][..]).expect("zero anchor reads") +} + +/// A proof-stamped bundle with the given actions, tachygrams, and covered-actions digest. +fn proven_bundle( + actions: Vec, + tachygrams: Vec, + coverage: [u8; 32], +) -> Bundle { + let tachygram_set = tachygrams.iter().copied().collect::(); + Bundle { + actions, + value_balance: value::Balance::ZERO, + binding_sig: bundle::Signature::read(&[0x02u8; 64][..]).expect("64 bytes"), + memo: Vec::new(), + stamp: ProofStamp { + coverage, + tachygram_set: tachygram_set.commit(), + tachygrams: tachygrams.into_iter().collect(), + anchor: zero_anchor(), + proof: Box::new(ragu::Proof::trivial()), + }, + } +} + +/// A pointer-stamped bundle with the given actions, naming `target` as its covering aggregate. +fn adjunct_bundle(actions: Vec, target: [u8; 64]) -> Bundle { + Bundle { + actions, + value_balance: value::Balance::ZERO, + binding_sig: bundle::Signature::read(&[0x02u8; 64][..]).expect("64 bytes"), + memo: Vec::new(), + stamp: PointerStamp::try_from(target).expect("nonzero wtxid"), + } +} + +/// A V7 transaction carrying only the given tachyon bundle. +fn v7_transaction(tachyon_bundle: TachyonBundle) -> Arc { + Arc::new(Transaction::V7 { + network_upgrade: NetworkUpgrade::NuTachyon, + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: Height(0), + inputs: Vec::new(), + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + ironwood_shielded_data: None, + tachyon_shielded_data: Some(tachyon_bundle.into()), + }) +} + +/// A block containing the given transactions (the header contents are irrelevant to coherence). +fn block_with(transactions: Vec>) -> Block { + let template = Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]) + .expect("hardcoded test block deserializes"); + + Block { + header: template.header, + transactions, + } +} + +fn tachygram(value: u64) -> Tachygram { + Tachygram::from(halo2::pasta::pallas::Base::from(value)) +} + +/// Rule 1: two proof stamps sharing a tachygram anywhere in the block are rejected. +#[test] +fn duplicate_tachygram_across_block_is_rejected() { + let _init_guard = zakura_test::init(); + + let action_a = dummy_action(); + let action_b = dummy_action(); + let digest_a = action_descriptor_digest(&[action_a.descriptor()]); + let digest_b = action_descriptor_digest(&[action_b.descriptor()]); + + let block = block_with(vec![ + v7_transaction(TachyonBundle::Proven(proven_bundle( + vec![action_a], + vec![tachygram(1), tachygram(2)], + digest_a, + ))), + v7_transaction(TachyonBundle::Proven(proven_bundle( + vec![action_b], + vec![tachygram(2), tachygram(3)], + digest_b, + ))), + ]); + + assert_eq!( + coherence(&block).err(), + Some(BlockError::DuplicateTachygram) + ); +} + +/// Rule 2: a pointer stamp naming a wtxid with no proof-stamped transaction in the block is +/// rejected. +#[test] +fn pointer_to_missing_aggregate_is_rejected() { + let _init_guard = zakura_test::init(); + + let block = block_with(vec![v7_transaction(TachyonBundle::Adjunct( + adjunct_bundle(vec![dummy_action()], [0xEEu8; 64]), + ))]); + + assert_eq!( + coherence(&block).err(), + Some(BlockError::TachyonAggregateNotFound) + ); +} + +/// Rule 3: a proof stamp whose carried covered-actions digest doesn't match its actions is +/// rejected. +#[test] +fn coverage_mismatch_is_rejected() { + let _init_guard = zakura_test::init(); + + let block = block_with(vec![v7_transaction(TachyonBundle::Proven(proven_bundle( + vec![dummy_action()], + vec![], + // Deliberately not the digest of the bundle's actions. + [0u8; 32], + )))]); + + assert_eq!( + coherence(&block).err(), + Some(BlockError::TachyonCoverageMismatch) + ); +} + +/// Rule 3: the same action descriptor covered twice (in the aggregate and in an adjunct naming +/// it) is rejected, even with a digest computed over the duplicated descriptor list. +#[test] +fn duplicate_covered_action_is_rejected() { + let _init_guard = zakura_test::init(); + + let action = dummy_action(); + let covered_digest = action_descriptor_digest(&[action.descriptor(), action.descriptor()]); + let aggregate_tx = v7_transaction(TachyonBundle::Proven(proven_bundle( + vec![action], + vec![tachygram(1)], + covered_digest, + ))); + + let aggregate_wtxid = WtxId::from(aggregate_tx.as_ref()).as_bytes(); + let adjunct_tx = v7_transaction(TachyonBundle::Adjunct(adjunct_bundle( + vec![action], + aggregate_wtxid, + ))); + + let block = block_with(vec![aggregate_tx, adjunct_tx]); + + assert_eq!( + coherence(&block).err(), + Some(BlockError::TachyonDuplicateAction) + ); +} + +/// Rules 2-3 positive path: an aggregate covering its own actions plus a pointing adjunct's +/// actions passes coherence, and the returned work item groups the adjunct with its aggregate. +#[test] +fn aggregate_with_adjunct_passes_coherence() { + let _init_guard = zakura_test::init(); + + let own_action = dummy_action(); + let adjunct_action = dummy_action(); + + let covered_digest = + action_descriptor_digest(&[own_action.descriptor(), adjunct_action.descriptor()]); + // Two tachygrams per covered action (the aggregate's own and the adjunct's). + let aggregate_tx = v7_transaction(TachyonBundle::Proven(proven_bundle( + vec![own_action], + vec![tachygram(1), tachygram(2), tachygram(3), tachygram(4)], + covered_digest, + ))); + + let aggregate_wtxid = WtxId::from(aggregate_tx.as_ref()).as_bytes(); + let adjunct_tx = v7_transaction(TachyonBundle::Adjunct(adjunct_bundle( + vec![adjunct_action], + aggregate_wtxid, + ))); + + // Self-validation: the hand-rolled digest must agree with tachyon's coverage check. + { + let Some(data) = aggregate_tx.tachyon_shielded_data() else { + panic!("aggregate transaction has a tachyon bundle"); + }; + let TachyonBundle::Proven(aggregate) = &data.0 else { + panic!("aggregate transaction bundle is proof-stamped"); + }; + let Some(data) = adjunct_tx.tachyon_shielded_data() else { + panic!("adjunct transaction has a tachyon bundle"); + }; + let TachyonBundle::Adjunct(adjunct) = &data.0 else { + panic!("adjunct transaction bundle is pointer-stamped"); + }; + aggregate + .verify_coverage(&[adjunct.as_dyn()]) + .expect("hand-rolled action_descriptor_digest must not drift from the tachyon crate"); + } + + let block = block_with(vec![aggregate_tx, adjunct_tx]); + + let aggregates = coherence(&block).expect("coherent block passes"); + assert_eq!(aggregates.len(), 1, "one proof stamp to verify"); + assert_eq!( + aggregates[0].adjuncts.len(), + 1, + "the work item groups the pointing adjunct with its aggregate", + ); +} + +/// An autonome (proof-stamped bundle covering only its own actions) passes coherence by itself. +#[test] +fn autonome_passes_coherence() { + let _init_guard = zakura_test::init(); + + let action = dummy_action(); + let digest = action_descriptor_digest(&[action.descriptor()]); + + let block = block_with(vec![v7_transaction(TachyonBundle::Proven(proven_bundle( + vec![action], + vec![tachygram(7), tachygram(8)], + digest, + )))]); + + let aggregates = coherence(&block).expect("coherent block passes"); + assert_eq!(aggregates.len(), 1); + assert!(aggregates[0].adjuncts.is_empty()); +} + +/// Rule 4 negative path: a proof stamp with a correct covered-actions digest but an invalid +/// (trivial mock) proof fails proof verification. +#[tokio::test(flavor = "multi_thread")] +async fn trivial_proof_fails_verification() { + let _init_guard = zakura_test::init(); + + let action = dummy_action(); + let digest = action_descriptor_digest(&[action.descriptor()]); + let aggregate = AggregateCoverage { + bundle: proven_bundle(vec![action], vec![tachygram(1)], digest), + adjuncts: Vec::new(), + }; + + let result = crate::primitives::tachyon::verify_proof_stamp(aggregate).await; + assert!( + matches!(result, Err(BlockError::TachyonProofInvalid(_))), + "expected TachyonProofInvalid, got: {result:?}", + ); +} + +/// Rule 4 positive path: a genuinely proven (mock proof system) output stamp verifies. +#[tokio::test(flavor = "multi_thread")] +async fn real_mock_proof_passes_verification() { + let _init_guard = zakura_test::init(); + + let mut rng = rand_10::rng(); + + let sk = private::SpendingKey::random(&mut rng); + let note = Note { + pk: sk.derive_payment_key(), + value: value::Positive::try_from(100u64).expect("valid value"), + psi: nullifier::Trapdoor::random(&mut rng), + rcm: CommitmentTrapdoor::random(&mut rng), + }; + let theta = ActionEntropy::random(&mut rng); + let rcv = value::Trapdoor::random(&mut rng); + let output_plan = action::Plan::output(note, theta, rcv); + + let plan = bundle::Plan::new(vec![], vec![output_plan]); + let pak = sk.derive_proof_private(); + let ask = sk.derive_auth_private(); + + let stamp = plan + .stamp_plan(zero_anchor()) + .prove(&mut rng, &pak, vec![]) + .expect("output-only stamp plan proves under the mock proof system"); + + // Proof verification never checks signatures, so the sighash is arbitrary here. + let bundle = plan + .sign(&mut rng, &[0u8; 32], &ask) + .expect("bundle plan signs") + .stamp(stamp); + + let aggregate = AggregateCoverage { + bundle, + adjuncts: Vec::new(), + }; + + crate::primitives::tachyon::verify_proof_stamp(aggregate) + .await + .expect("a genuinely proven stamp verifies"); +} diff --git a/crates/zakura-consensus/src/error.rs b/crates/zakura-consensus/src/error.rs index 76e323712f..ff09f7ca20 100644 --- a/crates/zakura-consensus/src/error.rs +++ b/crates/zakura-consensus/src/error.rs @@ -290,6 +290,18 @@ pub enum TransactionError { #[error("Ironwood proof has a non-canonical size")] IronwoodProofSize, + #[error("Tachyon action value commitment and verification key must not be identity: {0}")] + #[cfg(zcash_unstable = "nutachyon")] + TachyonIdentityAction(String), + + #[error("Tachyon action and binding signatures must verify: {0}")] + #[cfg(zcash_unstable = "nutachyon")] + TachyonSignatureInvalid(String), + + #[error("the mempool only accepts autonome Tachyon transactions")] + #[cfg(zcash_unstable = "nutachyon")] + NonAutonomeTachyon, + #[error("unexpected error")] Other(String), } @@ -492,6 +504,14 @@ impl TransactionError { Self::Balance(_) => consensus("transaction.balance"), Self::OrchardProofSize => consensus("transaction.orchard_proof_size"), Self::IronwoodProofSize => consensus("transaction.ironwood_proof_size"), + #[cfg(zcash_unstable = "nutachyon")] + Self::TachyonIdentityAction(_) => consensus("transaction.tachyon_identity_action"), + #[cfg(zcash_unstable = "nutachyon")] + Self::TachyonSignatureInvalid(_) => consensus("transaction.tachyon_signature_invalid"), + #[cfg(zcash_unstable = "nutachyon")] + Self::NonAutonomeTachyon => { + BodyVerificationClass::Retryable(TransientBodyFailureKind::VerifierUnavailable) + } Self::ImmatureTransparentCoinbaseSpend { .. } => { consensus("transaction.immature_transparent_coinbase_spend") } @@ -550,6 +570,9 @@ impl TransactionError { | LockedUntilAfterBlockHeight(_) | LockedUntilAfterBlockTime(_) => 100, + #[cfg(zcash_unstable = "nutachyon")] + TachyonIdentityAction(_) | TachyonSignatureInvalid(_) => 100, + // NU6.2 mempool transactions are invalid under NU6.3 rules, but // honest peers can relay them briefly while their chain tips converge. WrongConsensusBranchIdNu6_3GracePeriod => 0, @@ -717,6 +740,30 @@ pub enum BlockError { source: amount::Error, }, + #[error("all Tachygrams in a block must be distinct")] + #[cfg(zcash_unstable = "nutachyon")] + DuplicateTachygram, + + #[error("a Tachyon pointer stamp must refer to a proof-stamped transaction in the same block")] + #[cfg(zcash_unstable = "nutachyon")] + TachyonAggregateNotFound, + + #[error("a Tachyon proof stamp's covered-actions digest does not match its aggregate")] + #[cfg(zcash_unstable = "nutachyon")] + TachyonCoverageMismatch, + + #[error("action descriptors covered by a Tachyon proof stamp must be distinct")] + #[cfg(zcash_unstable = "nutachyon")] + TachyonDuplicateAction, + + #[error("a Tachyon proof stamp must publish exactly two Tachygrams per covered action")] + #[cfg(zcash_unstable = "nutachyon")] + TachyonTachygramArityMismatch, + + #[error("a Tachyon proof stamp must verify: {0}")] + #[cfg(zcash_unstable = "nutachyon")] + TachyonProofInvalid(String), + #[error("unexpected error occurred: {0}")] Other(String), } @@ -755,6 +802,13 @@ impl BlockError { | BadMerkleRoot { .. } | WrongTransactionConsensusBranchId | TooManyTransparentSignatureOperations { .. } => 100, + #[cfg(zcash_unstable = "nutachyon")] + DuplicateTachygram + | TachyonAggregateNotFound + | TachyonCoverageMismatch + | TachyonDuplicateAction + | TachyonTachygramArityMismatch + | TachyonProofInvalid(_) => 100, Transaction(err) => err.mempool_misbehavior_score(), _other => 0, } diff --git a/crates/zakura-consensus/src/primitives.rs b/crates/zakura-consensus/src/primitives.rs index eeed3cd999..5d62eeef2d 100644 --- a/crates/zakura-consensus/src/primitives.rs +++ b/crates/zakura-consensus/src/primitives.rs @@ -17,6 +17,8 @@ pub mod halo2; pub mod redjubjub; pub mod redpallas; pub mod sapling; +#[cfg(zcash_unstable = "nutachyon")] +pub mod tachyon; /// The maximum batch size for any of the batch verifiers. const MAX_BATCH_SIZE: usize = 64; diff --git a/crates/zakura-consensus/src/primitives/halo2.rs b/crates/zakura-consensus/src/primitives/halo2.rs index 6d2094a4ac..5f83455ef5 100644 --- a/crates/zakura-consensus/src/primitives/halo2.rs +++ b/crates/zakura-consensus/src/primitives/halo2.rs @@ -404,6 +404,9 @@ fn lazy_verifier_for(network_upgrade: NetworkUpgrade) -> &'static Lazy &VERIFIER_NU6_3_ONWARD, + #[cfg(zcash_unstable = "nutachyon")] + NuTachyon => &VERIFIER_NU6_3_ONWARD, + // `ZFuture` is post-NU6.3 and inherits the NU6.3 circuit. Keep the // cfg-gated arm explicit so a future upgrade cannot silently fall back // to an older or insecure verifier. diff --git a/crates/zakura-consensus/src/primitives/tachyon.rs b/crates/zakura-consensus/src/primitives/tachyon.rs new file mode 100644 index 0000000000..e82769b2cf --- /dev/null +++ b/crates/zakura-consensus/src/primitives/tachyon.rs @@ -0,0 +1,34 @@ +//! Asynchronous verification of Tachyon proof stamps. + +use crate::{block::tachyon::AggregateCoverage, error::BlockError}; + +use super::spawn_fifo; + +/// Verifies a Tachyon aggregate's proof stamp against all covered actions. +pub async fn verify_proof_stamp(aggregate: AggregateCoverage) -> Result<(), BlockError> { + spawn_fifo(move || { + let adjunct_refs: Vec<_> = aggregate + .adjuncts + .iter() + .map(|adjunct| adjunct.as_dyn()) + .collect(); + + match aggregate + .bundle + .verify_proof(&mut rand_10::rng(), &adjunct_refs) + { + Ok(true) => Ok(()), + Ok(false) => Err(BlockError::TachyonProofInvalid( + "proof stamp was disproved".to_string(), + )), + Err(error) => Err(BlockError::TachyonProofInvalid(error.to_string())), + } + }) + .await + .map_err(|_| { + BlockError::Other( + "threadpool unexpectedly dropped response channel sender; is Zakura shutting down?" + .to_string(), + ) + })? +} diff --git a/crates/zakura-consensus/src/transaction.rs b/crates/zakura-consensus/src/transaction.rs index 14bc9f4db7..3b96b4acc5 100644 --- a/crates/zakura-consensus/src/transaction.rs +++ b/crates/zakura-consensus/src/transaction.rs @@ -486,6 +486,12 @@ where // Transaction parsing enforces the same rule. Repeat it here for // defense-in-depth and transactions constructed in memory. check::shielded_proof_size_is_canonical(&tx)?; + #[cfg(zcash_unstable = "nutachyon")] + check::tachyon_actions_have_valid_digests(&tx)?; + #[cfg(zcash_unstable = "nutachyon")] + if req.is_mempool() { + check::tachyon_bundle_is_autonome(&tx)?; + } // Validate the coinbase input consensus rules if req.is_mempool() && tx.is_coinbase() { @@ -612,14 +618,22 @@ where cached_ffi_transaction.clone(), wtx_id.expect("a v5 transaction has a witnessed transaction ID"), )?, - Transaction::V6 { - .. - } => Self::verify_v6_transaction( + Transaction::V6 { .. } => { + Self::verify_v6_or_v7_transaction( + &req, + &network, + script_verifier, + cached_ffi_transaction.clone(), + wtx_id.expect("a v6 or v7 transaction has a witnessed transaction ID"), + )? + } + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => Self::verify_v6_or_v7_transaction( &req, &network, script_verifier, cached_ffi_transaction.clone(), - wtx_id.expect("a v6 transaction has a witnessed transaction ID"), + wtx_id.expect("a v6 or v7 transaction has a witnessed transaction ID"), )?, }; @@ -998,6 +1012,12 @@ where transaction.version(), network_upgrade, )), + + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => Err(TransactionError::UnsupportedByNetworkUpgrade( + transaction.version(), + network_upgrade, + )), } } @@ -1082,6 +1102,9 @@ where | NetworkUpgrade::Nu6_3 | NetworkUpgrade::Nu7 => Ok(()), + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => Ok(()), + #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => Ok(()), @@ -1099,8 +1122,8 @@ where } } - /// Verify a V6 transaction. - fn verify_v6_transaction( + /// Verify a V6 or V7 transaction. + fn verify_v6_or_v7_transaction( request: &Request, network: &Network, script_verifier: script::Verifier, @@ -1110,7 +1133,16 @@ where let transaction = request.transaction(); let nu = request.upgrade(network); - Self::verify_v6_transaction_network_upgrade(&transaction, nu)?; + match transaction.as_ref() { + Transaction::V6 { .. } => { + Self::verify_v6_transaction_network_upgrade(&transaction, nu)? + } + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => { + Self::verify_v7_transaction_network_upgrade(&transaction, nu)? + } + _ => unreachable!("V6/V7 verification is only called for V6 or V7 transactions"), + } let sapling_bundle = cached_ffi_transaction.sighasher().sapling_bundle(); let orchard_bundle = cached_ffi_transaction.sighasher().orchard_bundle(); @@ -1120,7 +1152,7 @@ where .sighasher() .sighash(HashType::ALL, None); - Ok(Self::verify_transparent_inputs_and_outputs( + let async_checks = Self::verify_transparent_inputs_and_outputs( request, script_verifier, cached_ffi_transaction, @@ -1141,7 +1173,13 @@ where &sighash, nu, wtx_id, - ))) + )); + + #[cfg(zcash_unstable = "nutachyon")] + let async_checks = + async_checks.and(Self::verify_tachyon_signatures(&transaction, &sighash)); + + Ok(async_checks) } /// Verifies if a V6 `transaction` is supported by `network_upgrade`. @@ -1159,6 +1197,46 @@ where Ok(()) } + /// Verifies if a V7 `transaction` is supported by `network_upgrade`. + #[cfg(zcash_unstable = "nutachyon")] + fn verify_v7_transaction_network_upgrade( + transaction: &Transaction, + network_upgrade: NetworkUpgrade, + ) -> Result<(), TransactionError> { + if network_upgrade < NetworkUpgrade::NuTachyon { + return Err(TransactionError::UnsupportedByNetworkUpgrade( + transaction.version(), + network_upgrade, + )); + } + + Ok(()) + } + + /// Queues verification of a V7 transaction's Tachyon action and binding signatures. + #[cfg(zcash_unstable = "nutachyon")] + fn verify_tachyon_signatures(tx: &Transaction, sighash: &SigHash) -> AsyncChecks { + use zcash_tachyon::TachyonBundle; + + let mut async_checks = AsyncChecks::new(); + let Some(tachyon_shielded_data) = tx.tachyon_shielded_data() else { + return async_checks; + }; + + let bundle = tachyon_shielded_data.0.clone(); + let sighash_bytes = sighash.0; + async_checks.push(primitives::spawn_fifo_and_convert(move || { + match &bundle { + TachyonBundle::NoBundle => Ok(()), + TachyonBundle::Proven(bundle) => bundle.verify_signatures(&sighash_bytes), + TachyonBundle::Adjunct(bundle) => bundle.verify_signatures(&sighash_bytes), + } + .map_err(|error| TransactionError::TachyonSignatureInvalid(error.to_string())) + })); + + async_checks + } + /// Verifies if a transaction's transparent inputs are valid using the provided /// `script_verifier` and `cached_ffi_transaction`. /// @@ -1346,7 +1424,7 @@ where /// /// `wtx_id` identifies the transaction containing `bundle`; the cache adds /// the bundle's value pool to distinguish the Orchard and Ironwood slots in - /// a v6 transaction. + /// a v6 or v7 transaction. fn verify_orchard_bundle( bundle: Option<::orchard::bundle::Bundle<::orchard::bundle::Authorized, ZatBalance>>, sighash: &SigHash, diff --git a/crates/zakura-consensus/src/transaction/check.rs b/crates/zakura-consensus/src/transaction/check.rs index 32550a6914..100da98a42 100644 --- a/crates/zakura-consensus/src/transaction/check.rs +++ b/crates/zakura-consensus/src/transaction/check.rs @@ -908,3 +908,35 @@ pub fn consensus_branch_id( Ok(()) } + +/// Checks that no Tachyon action's value commitment or verification key is the identity point. +#[cfg(zcash_unstable = "nutachyon")] +pub fn tachyon_actions_have_valid_digests(tx: &Transaction) -> Result<(), TransactionError> { + let Some(tachyon_shielded_data) = tx.tachyon_shielded_data() else { + return Ok(()); + }; + + for action in tachyon_shielded_data.actions() { + action + .digest() + .map_err(|error| TransactionError::TachyonIdentityAction(error.to_string()))?; + } + + Ok(()) +} + +/// Checks that a Tachyon transaction is autonome before it enters the mempool. +#[cfg(zcash_unstable = "nutachyon")] +pub fn tachyon_bundle_is_autonome(tx: &Transaction) -> Result<(), TransactionError> { + let Some(tachyon_shielded_data) = tx.tachyon_shielded_data() else { + return Ok(()); + }; + + match &tachyon_shielded_data.0 { + zcash_tachyon::TachyonBundle::Proven(bundle) if bundle.is_autonome() => Ok(()), + zcash_tachyon::TachyonBundle::NoBundle => Ok(()), + zcash_tachyon::TachyonBundle::Proven(_) | zcash_tachyon::TachyonBundle::Adjunct(_) => { + Err(TransactionError::NonAutonomeTachyon) + } + } +} diff --git a/crates/zakura-consensus/src/transaction/tests.rs b/crates/zakura-consensus/src/transaction/tests.rs index 415f71a058..7c5b60a02d 100644 --- a/crates/zakura-consensus/src/transaction/tests.rs +++ b/crates/zakura-consensus/src/transaction/tests.rs @@ -54,6 +54,8 @@ use super::{check, Request, Verifier}; #[cfg(test)] mod prop; +#[cfg(all(test, zcash_unstable = "nutachyon"))] +mod tachyon; /// Returns the timeout duration for tests, extended when running under coverage /// instrumentation to account for the performance overhead. diff --git a/crates/zakura-consensus/src/transaction/tests/prop.rs b/crates/zakura-consensus/src/transaction/tests/prop.rs index a2dc56bed9..10ae42fb7f 100644 --- a/crates/zakura-consensus/src/transaction/tests/prop.rs +++ b/crates/zakura-consensus/src/transaction/tests/prop.rs @@ -331,6 +331,18 @@ fn mock_transparent_transaction( ironwood_shielded_data: None, network_upgrade, }, + #[cfg(zcash_unstable = "nutachyon")] + 7 => Transaction::V7 { + inputs, + outputs, + lock_time, + expiry_height, + sapling_shielded_data: None, + orchard_shielded_data: None, + ironwood_shielded_data: None, + tachyon_shielded_data: None, + network_upgrade, + }, invalid_version => unreachable!("invalid transaction version: {}", invalid_version), }; @@ -360,6 +372,8 @@ fn sanitize_transaction_version( Nu5 | Nu6 | Nu6_1 | Nu6_2 => (4, 5), Nu6_3 => (4, 6), Nu7 => (5, 5), + #[cfg(zcash_unstable = "nutachyon")] + NuTachyon => (5, 7), #[cfg(zcash_unstable = "zfuture")] NetworkUpgrade::ZFuture => (5, u8::MAX), @@ -372,12 +386,14 @@ fn sanitize_transaction_version( } #[test] -fn sanitize_transaction_version_handles_v6_at_nu6_3() { +#[cfg(zcash_unstable = "nutachyon")] +fn sanitize_transaction_version_handles_v6_and_v7_upgrades() { let network = zakura_chain::parameters::testnet::Parameters::build() .with_activation_heights( zakura_chain::parameters::testnet::ConfiguredActivationHeights { nu6_3: Some(1), nu7: Some(2), + nu_tachyon: Some(3), ..Default::default() }, ) @@ -402,6 +418,14 @@ fn sanitize_transaction_version_handles_v6_at_nu6_3() { sanitize_transaction_version(&network, 6, block::Height(2)), (5, NetworkUpgrade::Nu7) ); + assert_eq!( + sanitize_transaction_version(&network, 6, block::Height(3)), + (6, NetworkUpgrade::NuTachyon) + ); + assert_eq!( + sanitize_transaction_version(&network, 7, block::Height(3)), + (7, NetworkUpgrade::NuTachyon) + ); } /// Create multiple mock transparent transfers. diff --git a/crates/zakura-consensus/src/transaction/tests/tachyon.rs b/crates/zakura-consensus/src/transaction/tests/tachyon.rs new file mode 100644 index 0000000000..fd0d372f72 --- /dev/null +++ b/crates/zakura-consensus/src/transaction/tests/tachyon.rs @@ -0,0 +1,406 @@ +//! Tests for V7 (tachyon) transaction verification. + +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use chrono::Utc; +use tower::{service_fn, ServiceExt}; + +use zakura_chain::{ + block::Height, + parameters::{testnet::ConfiguredActivationHeights, Network, NetworkUpgrade}, + transaction::{HashType, LockTime, Transaction}, +}; +use zcash_tachyon::{ + action, bundle, effect, + entropy::ActionEntropy, + keys::private, + note::{CommitmentTrapdoor, Note}, + nullifier, value, PointerStamp, ProofStamp, Tachygram, TachygramSetPoly, TachyonBundle, +}; + +use crate::{ + error::TransactionError, + transaction::{check, Request, Response, Verifier}, +}; + +/// A regtest network with NuTachyon (tachyon) scheduled, and NuTachyon's activation height. +fn nutachyon_network() -> (Network, Height) { + let network = Network::new_regtest( + ConfiguredActivationHeights { + canopy: Some(1), + nu5: Some(2), + nu6: Some(3), + nu6_1: Some(4), + nu6_2: Some(5), + nu6_3: Some(6), + nu7: Some(8), + nu_tachyon: Some(10), + ..Default::default() + } + .into(), + ); + let height = NetworkUpgrade::NuTachyon + .activation_height(&network) + .expect("NuTachyon activation height is configured"); + (network, height) +} + +/// A V7 transaction with the given tachyon bundle and no other transfers. +fn v7_transaction( + network_upgrade: NetworkUpgrade, + tachyon_bundle: Option, +) -> Transaction { + Transaction::V7 { + network_upgrade, + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: Height(0), + inputs: Vec::new(), + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + ironwood_shielded_data: None, + tachyon_shielded_data: tachyon_bundle.map(Into::into), + } +} + +/// A proof stamp with an unasserted covered-actions digest and no proof: tx-level verification +/// never checks the stamp's proof or coverage (those are block-level rules). +fn mock_proof_stamp(tachygrams: Vec) -> ProofStamp { + let tachygram_set = tachygrams.iter().copied().collect::(); + ProofStamp { + coverage: [0u8; 32], + tachygram_set: tachygram_set.commit(), + tachygrams: tachygrams.into_iter().collect(), + anchor: zcash_tachyon::Anchor::read(&[0u8; 64][..]).expect("zero anchor reads"), + proof: Box::new(ragu::Proof::trivial()), + } +} + +/// Computes the covered-actions digest used by a proof stamp. +fn action_descriptor_digest(actions: &[zcash_tachyon::Action]) -> [u8; 32] { + let mut descriptors: Vec<[u8; 64]> = actions.iter().map(|action| action.descriptor()).collect(); + descriptors.sort_unstable(); + + let mut state = blake2b_simd::Params::new() + .hash_length(32) + .personal(b"Tachyon-Actions") + .to_state(); + for descriptor in descriptors { + state.update(&descriptor); + } + state + .finalize() + .as_bytes() + .try_into() + .expect("hash length is 32") +} + +/// A random note worth `value` zatoshis, with its spending key. +fn random_note(rng: &mut impl rand_10::CryptoRng, value: u64) -> (private::SpendingKey, Note) { + let sk = private::SpendingKey::random(rng); + let note = Note { + pk: sk.derive_payment_key(), + value: value::Positive::try_from(value).expect("value is positive and below MAX_MONEY"), + psi: nullifier::Trapdoor::random(rng), + rcm: CommitmentTrapdoor::random(rng), + }; + (sk, note) +} + +/// A spend-only bundle plan worth `value`, with its spend authorizing key. +/// +/// A spend contributes its value positively to the transaction value pool, so the resulting +/// transaction has a valid (non-negative) miner fee. +fn spend_bundle_plan(value: u64) -> (bundle::Plan, private::SpendAuthorizingKey) { + let mut rng = rand_10::rng(); + + let (sk, note) = random_note(&mut rng, value); + let ask = sk.derive_auth_private(); + + let theta = ActionEntropy::random(&mut rng); + let rcv = value::Trapdoor::random(&mut rng); + let spend_plan = action::Plan::spend(note, theta, rcv, |alpha| { + ask.derive_action_private(&alpha).derive_action_public() + }); + + (bundle::Plan::new(vec![spend_plan], vec![]), ask) +} + +/// A properly-signed spend-only bundle over its own transaction's sighash, in the unproven state. +/// +/// The V7 sighash commits to the bundle's effecting data (action descriptors and value balance) +/// but not its signatures or stamp, so the sighash is computed from a draft bundle carrying +/// placeholder signatures, and the returned bundle then signs over it. Attaching the returned +/// bundle (under any stamp) to [`v7_transaction`] yields the same sighash the signatures commit +/// to. +fn signed_spend_bundle(value: u64) -> zcash_tachyon::Bundle { + let mut rng = rand_10::rng(); + + let (plan, ask) = spend_bundle_plan(value); + + let draft = plan + .sign(&mut rng, &[0u8; 32], &ask) + .expect("bundle plan has matching signatures and an in-range value balance"); + let draft_tx = v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Proven(draft.stamp(mock_proof_stamp(vec![])))), + ); + let sighash = v7_sighash(&draft_tx); + + plan.sign(&mut rng, &sighash, &ask) + .expect("bundle plan has matching signatures and an in-range value balance") +} + +/// The sighash the tachyon bundle's signatures must commit to. +/// +/// The V7 sighash commits to the tachyon bundle's effecting data (action descriptors and value +/// balance), but not its signatures or stamp — so a bundle with placeholder signatures produces +/// the same sighash as its properly-signed counterpart under any stamp. +fn v7_sighash(tx: &Transaction) -> [u8; 32] { + tx.sighash( + NetworkUpgrade::NuTachyon, + HashType::ALL, + Arc::new(Vec::new()), + None, + ) + .expect("sighash is computable for a V7 transaction with no transparent inputs") + .0 +} + +/// Verifies `tx` through the full transaction verifier with a block request at `height`. +async fn verify_block_transaction( + network: &Network, + height: Height, + tx: Transaction, +) -> Result { + // No transparent inputs anywhere in these tests, so the verifier never calls the state. + let state = service_fn(|_| async { unreachable!("state is not queried") }); + let verifier = Verifier::new_for_tests(network, state); + + verifier + .oneshot(Request::Block { + transaction_hash: tx.hash(), + transaction: Arc::new(tx), + known_utxos: Arc::new(HashMap::new()), + known_outpoint_hashes: Arc::new(HashSet::new()), + height, + time: Utc::now(), + }) + .await +} + +/// The mempool accepts only proof stamps that cover their own transaction's actions. +#[test] +fn mempool_accepts_only_autonome_tachyon_transactions() { + let bundle = signed_spend_bundle(100); + let mut stamp = mock_proof_stamp(vec![]); + stamp.coverage = action_descriptor_digest(&bundle.actions); + let autonome = bundle.stamp(stamp); + + let autonome_tx = v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Proven(autonome.clone())), + ); + assert_eq!(check::tachyon_bundle_is_autonome(&autonome_tx), Ok(())); + + let mut aggregate = autonome.clone(); + aggregate.stamp.coverage = [0xAA; 32]; + let aggregate_tx = v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Proven(aggregate)), + ); + assert_eq!( + check::tachyon_bundle_is_autonome(&aggregate_tx), + Err(TransactionError::NonAutonomeTachyon), + ); + + let adjunct_tx = v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Adjunct(autonome.strip( + PointerStamp::try_from([0xEE; 64]).expect("nonzero wtxid"), + ))), + ); + assert_eq!( + check::tachyon_bundle_is_autonome(&adjunct_tx), + Err(TransactionError::NonAutonomeTachyon), + ); +} + +/// The V7 sighash commits to the tachyon bundle's effecting data, but not its stamp. +#[tokio::test(flavor = "multi_thread")] +async fn v7_sighash_commits_to_tachyon_bundle() { + let _init_guard = zakura_test::init(); + + let mut rng = rand_10::rng(); + let (plan, ask) = spend_bundle_plan(100); + let bundle = plan + .sign(&mut rng, &[0u8; 32], &ask) + .expect("bundle plan has matching signatures and an in-range value balance"); + + let no_bundle_sighash = v7_sighash(&v7_transaction(NetworkUpgrade::NuTachyon, None)); + let proven_sighash = v7_sighash(&v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Proven( + bundle.clone().stamp(mock_proof_stamp(vec![])), + )), + )); + let adjunct_sighash = v7_sighash(&v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Adjunct( + bundle + .stamp(mock_proof_stamp(vec![])) + .strip(PointerStamp::try_from([0xEEu8; 64]).expect("nonzero wtxid")), + )), + )); + let other_bundle_sighash = v7_sighash(&v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Proven( + signed_spend_bundle(100).stamp(mock_proof_stamp(vec![])), + )), + )); + + // The sighash changes when a bundle is present, and differs between distinct action sets. + assert_ne!(no_bundle_sighash, proven_sighash); + assert_ne!(proven_sighash, other_bundle_sighash); + + // The stamp is excluded, so the sighash is invariant across stamping and stripping. + assert_eq!(proven_sighash, adjunct_sighash); +} + +/// A V7 transaction with a properly-signed tachyon bundle passes verification, whether +/// proof-stamped or pointer-stamped: proof coverage is a block-level rule. +#[tokio::test(flavor = "multi_thread")] +async fn v7_with_signed_tachyon_bundle_is_accepted() { + let _init_guard = zakura_test::init(); + let (network, height) = nutachyon_network(); + + // Proof-stamped: the tx-level rules don't verify the proof itself. + let proven = signed_spend_bundle(100).stamp(mock_proof_stamp(vec![])); + let tx = v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Proven(proven)), + ); + verify_block_transaction(&network, height, tx) + .await + .expect("a signed proof-stamped tachyon transaction should verify"); + + // Pointer-stamped: signature checks still run, proof coverage is deferred to the block. + // The sighash excludes the stamp, so stripping a signed bundle keeps its signatures valid. + let adjunct = signed_spend_bundle(100) + .stamp(mock_proof_stamp(vec![])) + .strip(PointerStamp::try_from([0xEEu8; 64]).expect("nonzero wtxid")); + let tx = v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Adjunct(adjunct)), + ); + verify_block_transaction(&network, height, tx) + .await + .expect("a signed pointer-stamped tachyon transaction should verify"); +} + +/// A tachyon bundle whose signatures don't commit to the transaction sighash is rejected. +#[tokio::test(flavor = "multi_thread")] +async fn v7_with_wrong_sighash_signatures_is_rejected() { + let _init_guard = zakura_test::init(); + let (network, height) = nutachyon_network(); + + // Sign over a different message than the transaction's sighash. + let mut rng = rand_10::rng(); + let (plan, ask) = spend_bundle_plan(100); + let bundle = plan + .sign(&mut rng, &[0x42u8; 32], &ask) + .expect("bundle plan has matching signatures and an in-range value balance") + .stamp(mock_proof_stamp(vec![])); + let tx = v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Proven(bundle)), + ); + + let result = verify_block_transaction(&network, height, tx).await; + assert!( + matches!(result, Err(TransactionError::TachyonSignatureInvalid(_))), + "expected TachyonSignatureInvalid, got: {result:?}", + ); +} + +/// A tachyon action whose value commitment is the identity point is rejected. +#[tokio::test(flavor = "multi_thread")] +async fn v7_with_identity_cv_is_rejected() { + use halo2::pasta::group::CurveAffine; + + let _init_guard = zakura_test::init(); + let (network, height) = nutachyon_network(); + + let mut rng = rand_10::rng(); + + // A structurally valid action, except its cv is the identity point. The identity check runs + // before signature verification, so the dummy signature bytes are never checked. + let (_, note) = random_note(&mut rng, 100); + let theta = ActionEntropy::random(&mut rng); + let alpha = theta.randomizer::(note.commitment()); + let rk = private::ActionSigningKey::new(&alpha).derive_action_public(); + + let action = zcash_tachyon::Action { + cv: value::Commitment::from(halo2::pasta::pallas::Affine::identity()), + rk, + sig: action::Signature::read(&[0x01u8; 64][..]).expect("64 bytes"), + }; + + let bundle = zcash_tachyon::Bundle { + actions: vec![action], + value_balance: value::Balance::ZERO, + binding_sig: bundle::Signature::read(&[0x02u8; 64][..]).expect("64 bytes"), + memo: Vec::new(), + stamp: mock_proof_stamp(vec![]), + }; + let tx = v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Proven(bundle)), + ); + + let result = verify_block_transaction(&network, height, tx).await; + assert!( + matches!(result, Err(TransactionError::TachyonIdentityAction(_))), + "expected TachyonIdentityAction, got: {result:?}", + ); +} + +// Distinctness of the tachygrams within one proof stamp is enforced at parse time in +// `zcash_tachyon`: they deserialize into a set, and the wire parser rejects duplicate or +// unsorted entries. A duplicate-tachygram transaction is unrepresentable in Zebra, so there +// is no transaction-level semantic check (or test) for it. + +/// V7 transactions are rejected before NuTachyon activation, including under NU7 +/// itself (the upstream v6-transaction upgrade). +#[tokio::test(flavor = "multi_thread")] +async fn v7_is_rejected_before_nu_tachyon() { + let _init_guard = zakura_test::init(); + let (network, _) = nutachyon_network(); + + let nu7_height = NetworkUpgrade::Nu7 + .activation_height(&network) + .expect("NU7 activation height is configured"); + + // The bundle's actions let the transaction pass the has-inputs-and-outputs check, so the + // network-upgrade gate is what rejects it. The gate errors before any signature check runs, + // so the signatures can be arbitrary. + let mut rng = rand_10::rng(); + let (plan, ask) = spend_bundle_plan(100); + let bundle = plan + .sign(&mut rng, &[0u8; 32], &ask) + .expect("bundle plan has matching signatures and an in-range value balance") + .stamp(mock_proof_stamp(vec![])); + let tx = v7_transaction( + NetworkUpgrade::NuTachyon, + Some(TachyonBundle::Proven(bundle)), + ); + + let result = verify_block_transaction(&network, nu7_height, tx).await; + assert!( + matches!(result, Err(TransactionError::WrongConsensusBranchId)), + "expected WrongConsensusBranchId, got: {result:?}", + ); +} diff --git a/crates/zakura-network/src/constants.rs b/crates/zakura-network/src/constants.rs index e77a0a2697..0e766731e1 100644 --- a/crates/zakura-network/src/constants.rs +++ b/crates/zakura-network/src/constants.rs @@ -342,11 +342,11 @@ pub const MAX_ADDRS_IN_ADDRESS_BOOK: usize = /// messages from each of our peers. pub const TIMESTAMP_TRUNCATION_SECONDS: u32 = 30 * 60; -// TODO: The NU7 protocol version is provisional. Update this constant and the mapping in -// `Version::min_specified_for_upgrade` once NU7's deployment ZIP is published. -// Next upgrade values, uncomment on activation: -// const CURRENT_NETWORK_PROTOCOL_VERSION_VALUE: u32 = 170_170; // NU7 Testnet. -// const CURRENT_NETWORK_PROTOCOL_VERSION_VALUE: u32 = 170_180; // NU7 Mainnet. +// TODO: The NU7 and NuTachyon protocol versions are provisional. Update this constant and the +// mapping in `Version::min_specified_for_upgrade` when their deployment ZIPs are published. +#[cfg(zcash_unstable = "nutachyon")] +const CURRENT_NETWORK_PROTOCOL_VERSION_VALUE: u32 = 170_190; // NuTachyon (Mainnet + Testnet). +#[cfg(not(zcash_unstable = "nutachyon"))] const CURRENT_NETWORK_PROTOCOL_VERSION_VALUE: u32 = 170_160; // NU6.3 (Mainnet + Testnet). /// The Zcash network protocol version implemented by this crate, and advertised diff --git a/crates/zakura-network/src/protocol/external/types.rs b/crates/zakura-network/src/protocol/external/types.rs index c61b70704e..b9ee01d6b6 100644 --- a/crates/zakura-network/src/protocol/external/types.rs +++ b/crates/zakura-network/src/protocol/external/types.rs @@ -120,14 +120,20 @@ impl Version { 170_150 } (Mainnet, Nu6_2) => 170_150, - // TODO: these NU6.3 (Ironwood) and Nu7 protocol versions are provisional, bumped above - // Nu6_2's 170_150. Update them when the real values are specified. + // TODO: these NU6.3, NU7, and NuTachyon protocol versions are provisional, bumped + // above Nu6_2's 170_150. Update them when the real values are specified. (Testnet(params), Nu6_3) if params.is_default_testnet() || params.is_regtest() => { 170_160 } (Mainnet, Nu6_3) => 170_160, (Testnet(params), Nu7) if params.is_default_testnet() || params.is_regtest() => 170_170, (Mainnet, Nu7) => 170_180, + #[cfg(zcash_unstable = "nutachyon")] + (Testnet(params), NuTachyon) if params.is_default_testnet() || params.is_regtest() => { + 170_190 + } + #[cfg(zcash_unstable = "nutachyon")] + (Mainnet, NuTachyon) => 170_190, // It should be fine to reject peers with earlier network protocol versions on custom testnets for now. (Testnet(_), _) => CURRENT_NETWORK_PROTOCOL_VERSION.0, @@ -219,11 +225,21 @@ mod test { let _init_guard = zakura_test::init(); let highest_network_upgrade = NetworkUpgrade::current(network, block::Height::MAX); + #[cfg(not(zcash_unstable = "nutachyon"))] assert!( matches!(highest_network_upgrade, Nu6 | Nu6_1 | Nu6_2 | Nu6_3 | Nu7), "expected coverage of all network upgrades: \ add the new network upgrade to the list in this test" ); + #[cfg(zcash_unstable = "nutachyon")] + assert!( + matches!( + highest_network_upgrade, + Nu6 | Nu6_1 | Nu6_2 | Nu6_3 | Nu7 | NuTachyon + ), + "expected coverage of all network upgrades: \ + add the new network upgrade to the list in this test" + ); for &network_upgrade in &[ BeforeOverwinter, @@ -238,6 +254,8 @@ mod test { Nu6_2, Nu6_3, Nu7, + #[cfg(zcash_unstable = "nutachyon")] + NuTachyon, ] { let height = network_upgrade.activation_height(network); if let Some(height) = height { @@ -250,19 +268,38 @@ mod test { } #[test] - fn nu63_protocol_versions_match_current_version() { + #[cfg(zcash_unstable = "nutachyon")] + fn nu_tachyon_protocol_versions_match_current_version() { let _init_guard = zakura_test::init(); assert_eq!( - Version::min_specified_for_upgrade(&Mainnet, Nu6_3), + Version::min_specified_for_upgrade(&Mainnet, NuTachyon), CURRENT_NETWORK_PROTOCOL_VERSION ); assert_eq!( - Version::min_specified_for_upgrade(&Network::new_default_testnet(), Nu6_3), + Version::min_specified_for_upgrade(&Network::new_default_testnet(), NuTachyon), + CURRENT_NETWORK_PROTOCOL_VERSION + ); + assert_eq!( + Version::min_specified_for_upgrade( + &Network::new_regtest(Default::default()), + NuTachyon + ), CURRENT_NETWORK_PROTOCOL_VERSION ); + } + + #[test] + #[cfg(not(zcash_unstable = "nutachyon"))] + fn nu63_protocol_versions_match_current_version() { + let _init_guard = zakura_test::init(); + assert_eq!( - Version::min_specified_for_upgrade(&Network::new_regtest(Default::default()), Nu6_3), + Version::min_specified_for_upgrade(&Mainnet, Nu6_3), + CURRENT_NETWORK_PROTOCOL_VERSION + ); + assert_eq!( + Version::min_specified_for_upgrade(&Network::new_default_testnet(), Nu6_3), CURRENT_NETWORK_PROTOCOL_VERSION ); } diff --git a/crates/zakura-network/src/zakura/block_sync/tests.rs b/crates/zakura-network/src/zakura/block_sync/tests.rs index 462da5ca72..9dab47c815 100644 --- a/crates/zakura-network/src/zakura/block_sync/tests.rs +++ b/crates/zakura-network/src/zakura/block_sync/tests.rs @@ -108,6 +108,8 @@ fn fake_block_at_height(template: &Arc, height: block::Height) -> | Transaction::V4 { inputs, .. } | Transaction::V5 { inputs, .. } => &mut inputs[0], Transaction::V6 { inputs, .. } => &mut inputs[0], + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { inputs, .. } => &mut inputs[0], }; match input { transparent::Input::Coinbase { diff --git a/crates/zakura-network/src/zakura/testkit/cluster.rs b/crates/zakura-network/src/zakura/testkit/cluster.rs index 4c4a02b63b..a9c8440bc7 100644 --- a/crates/zakura-network/src/zakura/testkit/cluster.rs +++ b/crates/zakura-network/src/zakura/testkit/cluster.rs @@ -925,6 +925,8 @@ mod tests { nu6_2: None, nu6_3: None, nu7: None, + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, #[cfg(zcash_unstable = "zfuture")] zfuture: None, }) diff --git a/crates/zakura-network/src/zakura/testkit/mock_blocksync.rs b/crates/zakura-network/src/zakura/testkit/mock_blocksync.rs index 1adc319c8f..bdfd2fef69 100644 --- a/crates/zakura-network/src/zakura/testkit/mock_blocksync.rs +++ b/crates/zakura-network/src/zakura/testkit/mock_blocksync.rs @@ -739,6 +739,8 @@ fn synthetic_block_at_height( | Transaction::V4 { inputs, .. } | Transaction::V5 { inputs, .. } => &mut inputs[0], Transaction::V6 { inputs, .. } => &mut inputs[0], + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { inputs, .. } => &mut inputs[0], }; match input { transparent::Input::Coinbase { diff --git a/crates/zakura-rpc/Cargo.toml b/crates/zakura-rpc/Cargo.toml index a6a320a482..aa653cb510 100644 --- a/crates/zakura-rpc/Cargo.toml +++ b/crates/zakura-rpc/Cargo.toml @@ -123,7 +123,14 @@ tokio-rustls = { version = "0.26.4", default-features = false, features = ["logg # Only used to read the validity dates of the configured RPC TLS certificates. der = "0.7.10" +[target.'cfg(zcash_unstable = "nutachyon")'.dependencies] +zcash_tachyon = { workspace = true } + +[target.'cfg(zcash_unstable = "nutachyon")'.dev-dependencies] +ragu = { workspace = true } + [dev-dependencies] +blake2b_simd = { workspace = true } bytes = { workspace = true } http-body = { workspace = true } insta = { workspace = true, features = ["redactions", "json", "ron"] } diff --git a/crates/zakura-rpc/src/config/mining.rs b/crates/zakura-rpc/src/config/mining.rs index 15583080d5..44ceca1134 100644 --- a/crates/zakura-rpc/src/config/mining.rs +++ b/crates/zakura-rpc/src/config/mining.rs @@ -57,12 +57,22 @@ pub struct Config { /// Mine blocks using Zakura's internal miner, without an external mining pool or equihash solver. /// - /// This experimental feature is only supported on regtest as it uses null solutions and skips checking - /// for a valid Proof of Work. + /// This experimental feature is only supported on proof-of-work-disabled test networks because + /// it uses null solutions. /// /// The internal miner is off by default. #[serde(default)] pub internal_miner: bool, + + /// Generate a deterministic Tachyon transaction workload for every internally mined block. + /// + /// This test-only workload replaces externally submitted mempool transactions. It pays miner + /// rewards to anyone-can-spend outputs, then shields each output into an independently derived + /// Tachyon recipient as soon as the coinbase matures. It is only accepted together with + /// [`Self::internal_miner`] on a proof-of-work-disabled test network. + #[cfg(zcash_unstable = "nutachyon")] + #[serde(default)] + pub tachyon_workload: bool, } impl Config { diff --git a/crates/zakura-rpc/src/methods.rs b/crates/zakura-rpc/src/methods.rs index ff694fa262..1a1cbb042f 100644 --- a/crates/zakura-rpc/src/methods.rs +++ b/crates/zakura-rpc/src/methods.rs @@ -139,6 +139,14 @@ use types::{ z_validate_address::ZValidateAddressResponse, }; +/// Value pool balances returned by the blockchain RPCs. +#[cfg(not(zcash_unstable = "nutachyon"))] +pub type BlockchainValuePoolBalances = [GetBlockchainInfoBalance; 6]; + +/// Value pool balances returned by the blockchain RPCs. +#[cfg(zcash_unstable = "nutachyon")] +pub type BlockchainValuePoolBalances = [GetBlockchainInfoBalance; 7]; + /// Calls a Tower service and maps readiness or call errors to /// [`server::error::LegacyCode::Misc`]. async fn call_service(service: S, request: Request) -> Result @@ -2754,6 +2762,14 @@ where precomputed_coinbase = wait_for_new_tip => { let chain_info = fetch_chain_info(read_state.clone()).await?; + // Each mature coinbase reward has one deterministic workload at its first + // spendable height. Do not let the internal miner race that full template + // with the usual empty, provisional tip-change template. + #[cfg(zcash_unstable = "nutachyon")] + if miner_params.tachyon_workload() { + continue; + } + let server_long_poll_id = LongPollInput::new( chain_info.tip_height, chain_info.tip_hash, @@ -2821,7 +2837,33 @@ where let height = chain_info.tip_height.next().map_misc_error()?; - // Randomly select some mempool transactions. + // Randomly select some mempool transactions, or replace them with the internal miner's + // self-funded Tachyon workload. + #[cfg(zcash_unstable = "nutachyon")] + let mempool_txs = if miner_params.tachyon_workload() { + let generated = types::get_block_template::generate_tachyon_workload_transactions( + &self.network, + height, + chain_info.tip_hash, + read_state.clone(), + ) + .await; + + #[cfg(test)] + let generated = generated.into_iter().map(|tx| (0, tx)).collect(); + + generated + } else { + select_mempool_transactions( + &self.network, + height, + miner_params, + mempool_txs, + mempool_tx_deps, + ) + }; + + #[cfg(not(zcash_unstable = "nutachyon"))] let mempool_txs = select_mempool_transactions( &self.network, height, @@ -2830,6 +2872,26 @@ where mempool_tx_deps, ); + #[cfg(all(test, zcash_unstable = "nutachyon"))] + let (dependency_depths, selected_txs): (Vec<_>, Vec<_>) = mempool_txs.into_iter().unzip(); + #[cfg(all(not(test), zcash_unstable = "nutachyon"))] + let selected_txs = mempool_txs; + + #[cfg(zcash_unstable = "nutachyon")] + let selected_txs = types::get_block_template::tachyon::aggregate_transactions( + self.network.clone(), + height, + chain_info.tip_hash, + read_state, + selected_txs, + ) + .await; + + #[cfg(all(test, zcash_unstable = "nutachyon"))] + let mempool_txs: Vec<_> = dependency_depths.into_iter().zip(selected_txs).collect(); + #[cfg(all(not(test), zcash_unstable = "nutachyon"))] + let mempool_txs = selected_txs; + tracing::debug!( selected_mempool_tx_hashes = ?mempool_txs .iter() @@ -3648,9 +3710,6 @@ pub struct EndOfService { estimated_time: i64, } -/// Type alias for the array of `GetBlockchainInfoBalance` objects -pub type BlockchainValuePoolBalances = [GetBlockchainInfoBalance; 6]; - fn deserialize_blockchain_value_pool_balances<'de, D>( deserializer: D, ) -> std::result::Result @@ -3678,20 +3737,40 @@ fn blockchain_value_pool_balances_from_vec( where E: serde::de::Error, { + let missing_pool_delta = value_pools + .iter() + .any(|pool| pool.value_delta().is_some() || pool.value_delta_zat().is_some()) + .then(Amount::zero); + match value_pools.len() { 5 => { - let ironwood_delta = value_pools - .iter() - .any(|pool| pool.value_delta().is_some() || pool.value_delta_zat().is_some()) - .then(Amount::zero); - value_pools.push(GetBlockchainInfoBalance::ironwood( Amount::zero(), - ironwood_delta, + missing_pool_delta, + )); + #[cfg(zcash_unstable = "nutachyon")] + value_pools.push(GetBlockchainInfoBalance::tachyon( + Amount::zero(), + missing_pool_delta, )); } - 6 => {} - len => return Err(E::invalid_length(len, &"five or six value pool balances")), + 6 => { + #[cfg(zcash_unstable = "nutachyon")] + value_pools.push(GetBlockchainInfoBalance::tachyon( + Amount::zero(), + missing_pool_delta, + )); + } + #[cfg(zcash_unstable = "nutachyon")] + 7 => {} + len => { + #[cfg(not(zcash_unstable = "nutachyon"))] + let expected = &"five or six value pool balances"; + #[cfg(zcash_unstable = "nutachyon")] + let expected = &"five, six, or seven value pool balances"; + + return Err(E::invalid_length(len, expected)); + } } value_pools diff --git a/crates/zakura-rpc/src/methods/tests/snapshot.rs b/crates/zakura-rpc/src/methods/tests/snapshot.rs index 5d05edac44..ee2cbbe78b 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshot.rs +++ b/crates/zakura-rpc/src/methods/tests/snapshot.rs @@ -1020,6 +1020,8 @@ pub async fn test_mining_rpcs( miner_memo: None, // TODO: Use default field values when optional features are enabled in tests #8183 internal_miner: true, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_workload: false, }; // nu5 block height diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@mainnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@mainnet_10.snap index 49b5c9decc..0c39f9a326 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@mainnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@mainnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -73,6 +73,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@testnet_10.snap index cb0bcbb39f..4f100179e1 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_1@testnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -73,6 +73,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap index 82a48b36fa..31b291622e 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@mainnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -128,6 +128,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap index 9652f62dc2..04e6350c28 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_2@testnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -128,6 +128,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@mainnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@mainnet_10.snap index 49b5c9decc..0c39f9a326 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@mainnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@mainnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -73,6 +73,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@testnet_10.snap index cb0bcbb39f..4f100179e1 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_hash_verbosity_default@testnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -73,6 +73,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@mainnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@mainnet_10.snap index 49b5c9decc..0c39f9a326 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@mainnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@mainnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -73,6 +73,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@testnet_10.snap index cb0bcbb39f..4f100179e1 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_1@testnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -73,6 +73,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap index 82a48b36fa..31b291622e 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@mainnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -128,6 +128,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap index 9652f62dc2..04e6350c28 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_2@testnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -128,6 +128,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@mainnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@mainnet_10.snap index 49b5c9decc..0c39f9a326 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@mainnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@mainnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -73,6 +73,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@testnet_10.snap index cb0bcbb39f..4f100179e1 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_block_verbose_height_verbosity_default@testnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: block --- { @@ -73,6 +73,14 @@ expression: block "monitored": false, "valueDelta": 0.0, "valueDeltaZat": 0 + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false, + "valueDelta": 0.0, + "valueDeltaZat": 0 } ], "trees": {}, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap index 78eb8be8ea..9e875dfdaa 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info@mainnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: info --- { @@ -55,6 +55,12 @@ expression: info "chainValue": 0.0, "chainValueZat": 0, "monitored": false + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false } ], "upgrades": { diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap index 5e65a23dec..68e6dd9de6 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info@testnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: info --- { @@ -55,6 +55,12 @@ expression: info "chainValue": 0.0, "chainValueZat": 0, "monitored": false + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false } ], "upgrades": { diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height@nu6testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height@nu6testnet_10.snap index 897d41b14c..63cb87889f 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height@nu6testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_blockchain_info_future_nu6_height@nu6testnet_10.snap @@ -1,5 +1,5 @@ --- -source: zebra-rpc/src/methods/tests/snapshot.rs +source: crates/zakura-rpc/src/methods/tests/snapshot.rs expression: info --- { @@ -55,6 +55,12 @@ expression: info "chainValue": 0.0, "chainValueZat": 0, "monitored": false + }, + { + "id": "tachyon", + "chainValue": 0.0, + "chainValueZat": 0, + "monitored": false } ], "upgrades": { diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_info@mainnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_info@mainnet_10.snap index 6aa8f2e1aa..b06015ec0e 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_info@mainnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_info@mainnet_10.snap @@ -6,7 +6,7 @@ expression: info "version": 100, "build": "v0.0.1", "subversion": "[SubVersion]", - "protocolversion": 170160, + "protocolversion": 170190, "blocks": 10, "connections": 0, "difficulty": 1.0, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_info@testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_info@testnet_10.snap index 894224b8b3..fc9a0e52cd 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_info@testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_info@testnet_10.snap @@ -6,7 +6,7 @@ expression: info "version": 100, "build": "v0.0.1", "subversion": "[SubVersion]", - "protocolversion": 170160, + "protocolversion": 170190, "blocks": 10, "connections": 0, "difficulty": 1.0, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_network_info@mainnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_network_info@mainnet_10.snap index 7f7eb1ccdb..aa90ae69e6 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_network_info@mainnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_network_info@mainnet_10.snap @@ -5,7 +5,7 @@ expression: get_network_info { "version": 100, "subversion": "RPC test", - "protocolversion": 170160, + "protocolversion": 170190, "localservices": "0000000000000001", "timeoffset": 0, "connections": 1, diff --git a/crates/zakura-rpc/src/methods/tests/snapshots/get_network_info@testnet_10.snap b/crates/zakura-rpc/src/methods/tests/snapshots/get_network_info@testnet_10.snap index 7f7eb1ccdb..aa90ae69e6 100644 --- a/crates/zakura-rpc/src/methods/tests/snapshots/get_network_info@testnet_10.snap +++ b/crates/zakura-rpc/src/methods/tests/snapshots/get_network_info@testnet_10.snap @@ -5,7 +5,7 @@ expression: get_network_info { "version": 100, "subversion": "RPC test", - "protocolversion": 170160, + "protocolversion": 170190, "localservices": "0000000000000001", "timeoffset": 0, "connections": 1, diff --git a/crates/zakura-rpc/src/methods/tests/utils.rs b/crates/zakura-rpc/src/methods/tests/utils.rs index 9d6dde1405..26ae2c7af4 100644 --- a/crates/zakura-rpc/src/methods/tests/utils.rs +++ b/crates/zakura-rpc/src/methods/tests/utils.rs @@ -22,6 +22,8 @@ pub fn fake_history_tree(network: &Network) -> Arc { &first_sapling_root, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .unwrap(); diff --git a/crates/zakura-rpc/src/methods/tests/vectors.rs b/crates/zakura-rpc/src/methods/tests/vectors.rs index 9f0d30e3b4..773d19a69f 100644 --- a/crates/zakura-rpc/src/methods/tests/vectors.rs +++ b/crates/zakura-rpc/src/methods/tests/vectors.rs @@ -3094,6 +3094,8 @@ async fn gbt_with(net: Network, addr: ZcashAddress) { extra_coinbase_data: None, miner_memo: None, internal_miner: true, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_workload: false, }; // nu5 block height @@ -3784,6 +3786,8 @@ async fn rpc_getdifficulty() { extra_coinbase_data: None, miner_memo: None, internal_miner: true, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_workload: false, }; // nu5 block height diff --git a/crates/zakura-rpc/src/methods/types/get_block_template.rs b/crates/zakura-rpc/src/methods/types/get_block_template.rs index df4702fb3b..da64eb0b31 100644 --- a/crates/zakura-rpc/src/methods/types/get_block_template.rs +++ b/crates/zakura-rpc/src/methods/types/get_block_template.rs @@ -3,6 +3,10 @@ pub mod constants; pub mod parameters; pub mod proposal; +#[cfg(zcash_unstable = "nutachyon")] +pub mod tachyon; +#[cfg(zcash_unstable = "nutachyon")] +mod tachyon_workload; pub mod zip317; #[cfg(test)] @@ -64,6 +68,11 @@ pub use parameters::{ GetBlockTemplateCapability, GetBlockTemplateParameters, GetBlockTemplateRequestMode, }; pub use proposal::{BlockProposalResponse, BlockTemplateTimeSource}; +#[cfg(zcash_unstable = "nutachyon")] +pub(crate) use tachyon_workload::{ + generate_transactions as generate_tachyon_workload_transactions, REDEEM_SCRIPT_HASH, + TRANSACTIONS_PER_BLOCK, +}; /// An alias to indicate that a usize value represents the depth of in-block dependencies of a /// transaction. @@ -442,6 +451,10 @@ pub struct MinerParams { /// /// Applies only if [`Self::addr`] contains a shielded component. memo: Option, + + /// Generate the internal miner's Tachyon test workload. + #[cfg(zcash_unstable = "nutachyon")] + tachyon_workload: bool, } /// Builds the coinbase input data for a block Zakura constructs. @@ -463,6 +476,30 @@ impl MinerParams { // (`extra_coinbase_data` is length-validated), not a recoverable error. #[allow(clippy::unwrap_in_result)] pub fn new(net: &Network, conf: config::mining::Config) -> Result { + #[cfg(zcash_unstable = "nutachyon")] + if conf.tachyon_workload && !conf.internal_miner { + return Err(MinerParamsError::TachyonWorkloadNeedsInternalMiner); + } + #[cfg(zcash_unstable = "nutachyon")] + if conf.tachyon_workload && (!net.is_a_test_network() || !net.disable_pow()) { + return Err(MinerParamsError::UnsupportedTachyonWorkloadNetwork); + } + + #[cfg(zcash_unstable = "nutachyon")] + let workload_enabled = conf.tachyon_workload; + + #[cfg(zcash_unstable = "nutachyon")] + let addr = if workload_enabled { + Address::Transparent(zcash_transparent::address::TransparentAddress::ScriptHash( + REDEEM_SCRIPT_HASH, + )) + } else { + conf.miner_address + .map(|addr| Address::try_from_zcash_address(net, addr)) + .ok_or(MinerParamsError::MissingAddr)?? + }; + + #[cfg(not(zcash_unstable = "nutachyon"))] let addr = conf .miner_address .map(|addr| Address::try_from_zcash_address(net, addr)) @@ -483,7 +520,13 @@ impl MinerParams { .map(|memo| MemoBytes::from_bytes(memo.as_bytes())) .transpose()?; - Ok(Self { addr, data, memo }) + Ok(Self { + addr, + data, + memo, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_workload: workload_enabled, + }) } /// Returns the miner address. @@ -501,6 +544,12 @@ impl MinerParams { self.memo.as_ref() } + /// Is the internal miner's Tachyon workload enabled? + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_workload(&self) -> bool { + self.tachyon_workload + } + /// Randomizes the memo. pub fn randomize_memo(&mut self) { let mut random = [0u8; 512]; @@ -522,6 +571,8 @@ impl From
for MinerParams { addr, data: None, memo: None, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_workload: false, } } } @@ -535,6 +586,12 @@ pub enum MinerParamsError { InvalidAddr(zcash_address::ConversionError<&'static str>), #[error(transparent)] InvalidMemo(#[from] zcash_protocol::memo::Error), + #[cfg(zcash_unstable = "nutachyon")] + #[error("tachyon_workload requires internal_miner = true")] + TachyonWorkloadNeedsInternalMiner, + #[cfg(zcash_unstable = "nutachyon")] + #[error("tachyon_workload is only supported on proof-of-work-disabled test networks")] + UnsupportedTachyonWorkloadNetwork, } impl From> for MinerParamsError { diff --git a/crates/zakura-rpc/src/methods/types/get_block_template/proposal.rs b/crates/zakura-rpc/src/methods/types/get_block_template/proposal.rs index fef79555c2..59cc0e9edd 100644 --- a/crates/zakura-rpc/src/methods/types/get_block_template/proposal.rs +++ b/crates/zakura-rpc/src/methods/types/get_block_template/proposal.rs @@ -218,6 +218,8 @@ pub fn proposal_block_from_template( | NetworkUpgrade::Nu6_2 | NetworkUpgrade::Nu6_3 | NetworkUpgrade::Nu7 => block_commitments_hash.bytes_in_serialized_order(), + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => block_commitments_hash.bytes_in_serialized_order(), _ => Err(SerializationError::Parse( "Zakura does not support generating pre-Canopy block templates", ))?, diff --git a/crates/zakura-rpc/src/methods/types/get_block_template/tachyon.rs b/crates/zakura-rpc/src/methods/types/get_block_template/tachyon.rs new file mode 100644 index 0000000000..332b1dd13f --- /dev/null +++ b/crates/zakura-rpc/src/methods/types/get_block_template/tachyon.rs @@ -0,0 +1,740 @@ +//! Tachyon transaction aggregation for mined block templates. + +use std::{ + collections::{BTreeMap, BTreeSet, HashSet}, + sync::Arc, + time::Duration, +}; + +use rand_10::rngs::ThreadRng; +use tokio::time::timeout; +use tower::{BoxError, Service, ServiceExt}; +use zakura_chain::{ + block::{self, Block}, + parameters::Network, + tachyon, + transaction::{Transaction, UnminedTx, VerifiedUnminedTx, WtxId}, +}; +use zakura_state::{ReadRequest, ReadResponse, TachyonMiningData}; +use zcash_tachyon::{Bundle, EpochIndex, PointerStamp, ProofStamp, TachyonBundle}; + +const STATE_QUERY_TIMEOUT: Duration = Duration::from_secs(5); +const AGGREGATION_TIMEOUT: Duration = Duration::from_secs(30); + +/// Aggregate the selected autonome Tachyon transactions without changing their effecting data. +/// +/// A Tachyon transaction is omitted unless its anchor and tachygrams are valid for the exact +/// candidate tip. Proving failures leave the remaining safe transactions autonome, so block +/// template generation remains available while aggregation is temporarily unavailable. +pub async fn aggregate_transactions( + network: Network, + candidate_height: block::Height, + tip_hash: block::Hash, + mut read_state: S, + transactions: Vec, +) -> Vec +where + S: Service + Send + Clone + 'static, + S::Future: Send + 'static, +{ + if !transactions + .iter() + .any(|tx| tx.transaction.transaction().has_tachyon_shielded_data()) + { + return transactions; + } + + let candidates = transactions + .iter() + .filter_map(|tx| autonome_bundle(tx.transaction.transaction())) + .collect::>(); + + if candidates.is_empty() { + return without_tachyon_transactions(transactions); + } + + let anchors = candidates + .iter() + .map(|bundle| tachyon::Anchor::from(bundle.stamp.anchor)) + .collect::>(); + let tachygrams = candidates + .into_iter() + .flat_map(|bundle| bundle.stamp.tachygrams.iter().copied()) + .map(tachyon::Tachygram::from) + .collect::>(); + + let request = ReadRequest::TachyonMiningData { + anchors, + tachygrams, + tip_hash, + candidate_height, + }; + let state_response = timeout(STATE_QUERY_TIMEOUT, async move { + read_state.ready().await?.call(request).await + }) + .await; + + let mining_data = match state_response { + Ok(Ok(ReadResponse::TachyonMiningData(Some(mining_data)))) => mining_data, + Ok(Ok(ReadResponse::TachyonMiningData(None))) => { + return without_tachyon_transactions(transactions); + } + Ok(Ok(_)) => unreachable!("TachyonMiningData requests have matching responses"), + Ok(Err(error)) => { + tracing::warn!(?error, "could not load Tachyon mining data"); + return without_tachyon_transactions(transactions); + } + Err(_) => { + tracing::warn!("timed out loading Tachyon mining data"); + return without_tachyon_transactions(transactions); + } + }; + + let transactions = valid_for_candidate(&network, candidate_height, transactions, &mining_data); + if transactions + .iter() + .filter_map(|tx| autonome_bundle(tx.transaction.transaction())) + .count() + < 2 + { + return transactions; + } + + let fallback_transactions = transactions.clone(); + let aggregation = tokio::task::spawn_blocking(move || { + aggregate_with_data(&network, transactions, mining_data) + }); + + match timeout(AGGREGATION_TIMEOUT, aggregation).await { + Ok(Ok(Ok(transactions))) => transactions, + Ok(Ok(Err(error))) => { + tracing::warn!(%error, "could not aggregate Tachyon transactions"); + fallback_transactions + } + Ok(Err(error)) => { + tracing::warn!(?error, "Tachyon aggregation task failed"); + fallback_transactions + } + Err(_) => { + tracing::warn!("timed out aggregating Tachyon transactions"); + fallback_transactions + } + } +} + +fn valid_for_candidate( + network: &Network, + candidate_height: block::Height, + transactions: Vec, + mining_data: &TachyonMiningData, +) -> Vec { + let mut seen_descriptors = BTreeSet::new(); + let mut seen_tachygrams = BTreeSet::new(); + let mut omitted_ids = HashSet::new(); + let mut valid = Vec::with_capacity(transactions.len()); + + // ZIP-317 selection orders parents before descendants, so recording every omitted mined ID + // removes its dependent transactions transitively in this single pass. + for transaction in transactions { + let mined_id = transaction.transaction.id().mined_id(); + if transaction + .transaction + .transaction() + .spent_outpoints() + .any(|outpoint| omitted_ids.contains(&outpoint.hash)) + { + omitted_ids.insert(mined_id); + continue; + } + + let Some(data) = transaction + .transaction + .transaction() + .tachyon_shielded_data() + else { + valid.push(transaction); + continue; + }; + let TachyonBundle::Proven(bundle) = &data.0 else { + omitted_ids.insert(mined_id); + continue; + }; + + if !bundle.is_autonome() || bundle.verify_coverage(&[]).is_err() { + omitted_ids.insert(mined_id); + continue; + } + + let anchor = tachyon::Anchor::from(bundle.stamp.anchor); + let Some(&anchor_height) = mining_data.anchor_heights.get(&anchor) else { + omitted_ids.insert(mined_id); + continue; + }; + if !tachyon::within_scan_window(network, anchor_height, candidate_height) { + omitted_ids.insert(mined_id); + continue; + } + + let descriptors = bundle + .actions + .iter() + .map(|action| action.descriptor()) + .collect::>(); + if descriptors + .iter() + .any(|item| seen_descriptors.contains(item)) + { + omitted_ids.insert(mined_id); + continue; + } + + let tachygrams = bundle + .stamp + .tachygrams + .iter() + .copied() + .collect::>(); + if tachygrams.iter().any(|item| { + seen_tachygrams.contains(item) + || mining_data + .revealed_tachygrams + .contains(&tachyon::Tachygram::from(*item)) + }) { + omitted_ids.insert(mined_id); + continue; + } + + seen_descriptors.extend(descriptors); + seen_tachygrams.extend(tachygrams); + valid.push(transaction); + } + + valid +} + +fn without_tachyon_transactions(transactions: Vec) -> Vec { + let mut omitted_ids = HashSet::new(); + let mut valid = Vec::with_capacity(transactions.len()); + + // Preserve dependency closure when a Tachyon parent is omitted. + for transaction in transactions { + let mined_id = transaction.transaction.id().mined_id(); + let depends_on_omitted = transaction + .transaction + .transaction() + .spent_outpoints() + .any(|outpoint| omitted_ids.contains(&outpoint.hash)); + + if transaction + .transaction + .transaction() + .has_tachyon_shielded_data() + || depends_on_omitted + { + omitted_ids.insert(mined_id); + } else { + valid.push(transaction); + } + } + + valid +} + +fn aggregate_with_data( + network: &Network, + mut transactions: Vec, + mining_data: TachyonMiningData, +) -> Result, String> { + let mut epoch_groups = BTreeMap::>::new(); + + for (index, transaction) in transactions.iter().enumerate() { + let Some(bundle) = autonome_bundle(transaction.transaction.transaction()) else { + continue; + }; + let anchor = tachyon::Anchor::from(bundle.stamp.anchor); + let Some(&height) = mining_data.anchor_heights.get(&anchor) else { + continue; + }; + let Some(epoch) = tachyon::epoch(network, height) else { + continue; + }; + + epoch_groups.entry(epoch).or_default().push((index, height)); + } + + let mut rng = rand_10::rng(); + for (epoch, group) in epoch_groups { + if group.len() < 2 { + continue; + } + + aggregate_epoch_group( + &mut rng, + &mut transactions, + &mining_data.blocks, + EpochIndex(epoch), + &group, + )?; + } + + Ok(transactions) +} + +fn aggregate_epoch_group( + rng: &mut ThreadRng, + transactions: &mut [VerifiedUnminedTx], + blocks: &BTreeMap>, + epoch: EpochIndex, + group: &[(usize, block::Height)], +) -> Result<(), String> { + let target_height = group + .iter() + .map(|(_index, height)| *height) + .max() + .expect("non-empty aggregation groups have a target height"); + let target_anchor = group + .iter() + .find_map(|(index, height)| { + (*height == target_height) + .then(|| autonome_bundle(transactions[*index].transaction.transaction())) + .flatten() + .map(|bundle| bundle.stamp.anchor) + }) + .expect("group transactions have autonome bundles"); + + let mut lifted_stamps = Vec::with_capacity(group.len()); + + for &(index, start_height) in group { + let bundle = autonome_bundle(transactions[index].transaction.transaction()) + .expect("group transactions have autonome bundles"); + let descriptors = bundle + .actions + .iter() + .map(|action| action.descriptor()) + .collect::>(); + let mut lifted_bundle = bundle.clone(); + + if lifted_bundle.stamp.anchor != target_anchor { + let mut next_bundles = Vec::new(); + for height_value in (start_height.0 + 1)..=target_height.0 { + let height = block::Height(height_value); + let block = blocks + .get(&height) + .ok_or_else(|| format!("missing block at {height:?} for Tachyon lift"))?; + next_bundles.extend( + block + .transactions + .iter() + .filter_map(|transaction| proof_bundle(transaction)), + ); + } + + lifted_bundle = lifted_bundle + .lift(rng, &[], (epoch, &next_bundles)) + .map_err(|error| format!("could not lift Tachyon proof stamp: {error}"))?; + } + + lifted_stamps.push((lifted_bundle.stamp, descriptors)); + } + + let mut lifted_stamps = lifted_stamps.into_iter(); + let (mut merged_stamp, mut merged_descriptors) = lifted_stamps + .next() + .expect("non-empty aggregation groups have a first stamp"); + for (stamp, descriptors) in lifted_stamps { + let combined_descriptors = merged_descriptors + .union(&descriptors) + .copied() + .collect::>(); + merged_stamp = ProofStamp::merge( + rng, + (merged_stamp, merged_descriptors), + (stamp, descriptors), + ) + .map_err(|error| format!("could not merge Tachyon proof stamps: {error}"))?; + merged_descriptors = combined_descriptors; + } + + let aggregate_index = group[0].0; + let mut aggregate_bundle = + autonome_bundle(transactions[aggregate_index].transaction.transaction()) + .expect("group transactions have autonome bundles") + .clone(); + aggregate_bundle.stamp = merged_stamp; + replace_bundle( + &mut transactions[aggregate_index], + TachyonBundle::Proven(aggregate_bundle), + ); + + let aggregate_wtxid = WtxId::from( + transactions[aggregate_index] + .transaction + .transaction() + .as_ref(), + ) + .as_bytes(); + let pointer = PointerStamp::try_from(aggregate_wtxid) + .map_err(|error| format!("invalid Tachyon aggregate wtxid: {error}"))?; + + for &(index, _height) in &group[1..] { + let adjunct = autonome_bundle(transactions[index].transaction.transaction()) + .expect("group transactions have autonome bundles") + .clone() + .strip(pointer); + replace_bundle(&mut transactions[index], TachyonBundle::Adjunct(adjunct)); + } + + Ok(()) +} + +fn autonome_bundle(transaction: &Transaction) -> Option<&Bundle> { + let bundle = proof_bundle(transaction)?; + bundle.is_autonome().then_some(bundle) +} + +fn proof_bundle(transaction: &Transaction) -> Option<&Bundle> { + let data = transaction.tachyon_shielded_data()?; + let TachyonBundle::Proven(bundle) = &data.0 else { + return None; + }; + Some(bundle) +} + +fn replace_bundle(transaction: &mut VerifiedUnminedTx, bundle: TachyonBundle) { + let mut mined_transaction = transaction.transaction.transaction().as_ref().clone(); + let Transaction::V7 { + tachyon_shielded_data, + .. + } = &mut mined_transaction + else { + unreachable!("Tachyon bundles only appear in V7 transactions"); + }; + *tachyon_shielded_data = Some(bundle.into()); + transaction.transaction = UnminedTx::from(Arc::new(mined_transaction)); +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use zakura_chain::{ + amount::Amount, + block::{Block, Height}, + parameters::{testnet::ConfiguredActivationHeights, NetworkUpgrade}, + serialization::ZcashDeserialize, + transaction::LockTime, + transparent, + }; + use zcash_tachyon::{ + entropy::ActionEntropy, + keys::private, + note::{CommitmentTrapdoor, Note}, + nullifier, + stamp::StampState as _, + value, Anchor, TachygramSetPoly, + }; + + use super::*; + + #[test] + fn same_anchor_autonome_transactions_are_aggregated() { + let network = nutachyon_network(); + let anchor = Anchor::read(&[0u8; 32][..]).expect("zero anchor reads"); + let original = vec![verified_transaction(anchor), verified_transaction(anchor)]; + let original_ids = original + .iter() + .map(|tx| tx.transaction.id().mined_id()) + .collect::>(); + let mining_data = TachyonMiningData { + tip_anchor: tachyon::Anchor::from(anchor), + anchor_heights: HashMap::from([(tachyon::Anchor::from(anchor), Height(10))]), + blocks: BTreeMap::new(), + revealed_tachygrams: HashSet::new(), + }; + + let original = valid_for_candidate(&network, Height(11), original, &mining_data); + let aggregated = aggregate_with_data(&network, original, mining_data) + .expect("same-anchor stamps can be merged"); + + assert_aggregated(&aggregated, &original_ids); + } + + #[test] + fn older_autonome_stamp_is_lifted_before_aggregation() { + let network = nutachyon_network(); + let start_anchor = Anchor::read(&[0u8; 32][..]).expect("zero anchor reads"); + let intervening_transaction = verified_transaction(start_anchor) + .transaction + .transaction() + .clone(); + let intervening_bundle = proof_bundle(&intervening_transaction) + .expect("intervening transaction has a proof-stamped bundle"); + let target_anchor = start_anchor + .next_stamp(EpochIndex(0), &intervening_bundle.stamp.tachygram_set) + .expect("intervening stamp advances the anchor"); + let original = vec![ + verified_transaction(start_anchor), + verified_transaction(target_anchor), + ]; + let original_ids = original + .iter() + .map(|tx| tx.transaction.id().mined_id()) + .collect::>(); + + let mut intervening_block = + Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]) + .expect("hardcoded genesis block deserializes"); + intervening_block.transactions = vec![intervening_transaction]; + let mining_data = TachyonMiningData { + tip_anchor: tachyon::Anchor::from(target_anchor), + anchor_heights: HashMap::from([ + (tachyon::Anchor::from(start_anchor), Height(10)), + (tachyon::Anchor::from(target_anchor), Height(11)), + ]), + blocks: BTreeMap::from([(Height(11), Arc::new(intervening_block))]), + revealed_tachygrams: HashSet::new(), + }; + + let original = valid_for_candidate(&network, Height(12), original, &mining_data); + let aggregated = aggregate_with_data(&network, original, mining_data) + .expect("the older stamp can be lifted and merged"); + + assert_aggregated(&aggregated, &original_ids); + let TachyonBundle::Proven(aggregate) = &aggregated[0] + .transaction + .transaction() + .tachyon_shielded_data() + .expect("aggregate has Tachyon data") + .0 + else { + panic!("the first transaction should carry the aggregate proof"); + }; + assert_eq!(aggregate.stamp.anchor, target_anchor); + } + + #[test] + fn stale_anchor_transactions_are_omitted() { + let network = nutachyon_network(); + let anchor = Anchor::read(&[0u8; 32][..]).expect("zero anchor reads"); + let parent = verified_transaction(anchor); + let child = verified_dependent_transaction(parent.transaction.id().mined_id()); + let transactions = vec![parent, child]; + let mining_data = TachyonMiningData { + tip_anchor: tachyon::Anchor::from(anchor), + anchor_heights: HashMap::from([(tachyon::Anchor::from(anchor), Height(10))]), + blocks: BTreeMap::new(), + revealed_tachygrams: HashSet::new(), + }; + let candidate_height = Height(10 + 2 * tachyon::EPOCH_LENGTH); + + assert!( + valid_for_candidate(&network, candidate_height, transactions, &mining_data).is_empty(), + "stale-anchor transactions and their descendants must not enter a block template", + ); + } + + #[test] + fn already_revealed_tachygram_transactions_are_omitted() { + let network = nutachyon_network(); + let anchor = Anchor::read(&[0u8; 32][..]).expect("zero anchor reads"); + let transaction = verified_transaction(anchor); + let revealed = autonome_bundle(transaction.transaction.transaction()) + .expect("test transaction is autonome") + .stamp + .tachygrams + .iter() + .next() + .copied() + .expect("test stamp has tachygrams"); + let mining_data = TachyonMiningData { + tip_anchor: tachyon::Anchor::from(anchor), + anchor_heights: HashMap::from([(tachyon::Anchor::from(anchor), Height(10))]), + blocks: BTreeMap::new(), + revealed_tachygrams: HashSet::from([tachyon::Tachygram::from(revealed)]), + }; + + assert!( + valid_for_candidate(&network, Height(11), vec![transaction], &mining_data).is_empty(), + "a tachygram already revealed in the scan window must not enter a block template", + ); + } + + fn nutachyon_network() -> Network { + Network::new_regtest( + ConfiguredActivationHeights { + canopy: Some(1), + nu5: Some(2), + nu6: Some(3), + nu6_1: Some(4), + nu6_2: Some(5), + nu6_3: Some(6), + nu7: Some(8), + nu_tachyon: Some(10), + ..Default::default() + } + .into(), + ) + } + + fn assert_aggregated( + aggregated: &[VerifiedUnminedTx], + original_ids: &[zakura_chain::transaction::Hash], + ) { + assert_eq!( + aggregated + .iter() + .map(|tx| tx.transaction.id().mined_id()) + .collect::>(), + original_ids, + "aggregation must not change mined transaction IDs", + ); + + let TachyonBundle::Proven(aggregate) = &aggregated[0] + .transaction + .transaction() + .tachyon_shielded_data() + .expect("aggregate has Tachyon data") + .0 + else { + panic!("the first transaction should carry the aggregate proof"); + }; + let TachyonBundle::Adjunct(adjunct) = &aggregated[1] + .transaction + .transaction() + .tachyon_shielded_data() + .expect("adjunct has Tachyon data") + .0 + else { + panic!("the second transaction should point to the aggregate"); + }; + + assert!(aggregate.is_aggregate()); + let adjunct_dyn: &Bundle = adjunct; + assert!(aggregate.is_covering(&[adjunct_dyn])); + assert_eq!( + adjunct.stamp.stamp_digest(), + WtxId::from(aggregated[0].transaction.transaction().as_ref()).as_bytes(), + ); + } + + fn verified_transaction(anchor: Anchor) -> VerifiedUnminedTx { + let bundle = autonome_bundle_at(anchor); + let transaction = Transaction::V7 { + network_upgrade: NetworkUpgrade::NuTachyon, + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: Height(0), + inputs: Vec::new(), + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + ironwood_shielded_data: None, + tachyon_shielded_data: Some(TachyonBundle::Proven(bundle).into()), + }; + + VerifiedUnminedTx::new( + UnminedTx::from(transaction), + Amount::try_from(100_000u64).expect("fee is in range"), + 0, + 0, + Arc::new(Vec::new()), + ) + .expect("test transaction pays the conventional fee") + } + + fn verified_dependent_transaction( + parent: zakura_chain::transaction::Hash, + ) -> VerifiedUnminedTx { + let transaction = Transaction::V7 { + network_upgrade: NetworkUpgrade::NuTachyon, + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: Height(0), + inputs: vec![transparent::Input::PrevOut { + outpoint: transparent::OutPoint { + hash: parent, + index: 0, + }, + unlock_script: transparent::Script::new(&[]), + sequence: 0, + }], + outputs: vec![transparent::Output::new( + Amount::zero(), + transparent::Script::new(&[]), + )], + sapling_shielded_data: None, + orchard_shielded_data: None, + ironwood_shielded_data: None, + tachyon_shielded_data: None, + }; + + VerifiedUnminedTx::new( + UnminedTx::from(transaction), + Amount::try_from(100_000u64).expect("fee is in range"), + 0, + 0, + Arc::new(Vec::new()), + ) + .expect("test transaction pays the conventional fee") + } + + fn autonome_bundle_at(anchor: Anchor) -> Bundle { + let mut rng = rand_10::rng(); + let spending_key = private::SpendingKey::random(&mut rng); + let note = Note { + pk: spending_key.derive_payment_key(), + value: value::Positive::try_from(100u64).expect("positive value"), + psi: nullifier::Trapdoor::random(&mut rng), + rcm: CommitmentTrapdoor::random(&mut rng), + }; + let padding_note = Note { + pk: spending_key.derive_payment_key(), + value: value::Positive::try_from(1u64).expect("positive value"), + psi: nullifier::Trapdoor::random(&mut rng), + rcm: CommitmentTrapdoor::random(&mut rng), + }; + let tachygrams = + BTreeSet::from([note.commitment().into(), padding_note.commitment().into()]); + let ask = spending_key.derive_auth_private(); + let theta = ActionEntropy::random(&mut rng); + let rcv = value::Trapdoor::random(&mut rng); + let spend = zcash_tachyon::action::Plan::spend(note, theta, rcv, |alpha| { + ask.derive_action_private(&alpha).derive_action_public() + }); + let plan = zcash_tachyon::bundle::Plan::new(vec![spend], vec![]); + let bundle = plan + .sign(&mut rng, &[0u8; 32], &ask) + .expect("test bundle signs"); + let coverage = action_descriptor_digest(&bundle.actions); + let tachygram_set = tachygrams + .iter() + .copied() + .collect::() + .commit(); + + bundle.stamp(ProofStamp { + coverage, + anchor, + tachygram_set, + tachygrams, + proof: Box::new(ragu::Proof::trivial()), + }) + } + + fn action_descriptor_digest(actions: &[zcash_tachyon::Action]) -> [u8; 32] { + let mut descriptors: Vec<[u8; 64]> = + actions.iter().map(|action| action.descriptor()).collect(); + descriptors.sort_unstable(); + + let mut state = blake2b_simd::Params::new() + .hash_length(32) + .personal(b"Tachyon-Actions") + .to_state(); + for descriptor in descriptors { + state.update(&descriptor); + } + state + .finalize() + .as_bytes() + .try_into() + .expect("hash length is 32") + } +} diff --git a/crates/zakura-rpc/src/methods/types/get_block_template/tachyon_workload.rs b/crates/zakura-rpc/src/methods/types/get_block_template/tachyon_workload.rs new file mode 100644 index 0000000000..932aac90a7 --- /dev/null +++ b/crates/zakura-rpc/src/methods/types/get_block_template/tachyon_workload.rs @@ -0,0 +1,431 @@ +//! Internally generated Tachyon transactions for proof-of-work-disabled test networks. + +use std::{collections::HashSet, sync::Arc, time::Duration}; + +use rand_10::{rngs::StdRng, SeedableRng}; +use tokio::time::timeout; +use tower::{BoxError, Service, ServiceExt}; +use zcash_tachyon::{ + action, bundle, + entropy::ActionEntropy, + keys::private, + note::{CommitmentTrapdoor, Note}, + nullifier, value, TachyonBundle, +}; + +use zakura_chain::{ + amount::Amount, + block::{self, Height}, + parameters::{Network, NetworkUpgrade}, + transaction::{HashType, LockTime, Transaction, UnminedTx, VerifiedUnminedTx}, + transparent::{self, MIN_TRANSPARENT_COINBASE_MATURITY}, +}; +use zakura_state::{ReadRequest, ReadResponse}; + +/// The number of independently proof-stamped transactions generated in each eligible block. +pub const TRANSACTIONS_PER_BLOCK: usize = 3; + +/// HASH160 of the one-byte `OP_TRUE` redeem script used by the workload's coinbase outputs. +pub const REDEEM_SCRIPT_HASH: [u8; 20] = [ + 0xda, 0x17, 0x45, 0xe9, 0xb5, 0x49, 0xbd, 0x0b, 0xfa, 0x1a, 0x56, 0x99, 0x71, 0xc7, 0x7e, 0xba, + 0x30, 0xcd, 0x5a, 0x4b, +]; + +const OP_TRUE: u8 = 0x51; +const WORKLOAD_FEE: u64 = 50_000; +const STATE_QUERY_TIMEOUT: Duration = Duration::from_secs(5); + +/// Generate transactions that shield the first newly mature internal-miner reward into Tachyon. +/// +/// The miner reward at each height is split into [`TRANSACTIONS_PER_BLOCK`] outputs. Exactly when +/// that reward matures, this function spends every matching output into a separately generated +/// Tachyon recipient. Recipient randomness and mock-Ragu proof randomness are seeded by the source +/// outpoint, so repeated templates at the same height contain identical transactions. +pub async fn generate_transactions( + network: &Network, + candidate_height: Height, + tip_hash: block::Hash, + read_state: S, +) -> Vec +where + S: Service + Send + Clone + 'static, + S::Future: Send + 'static, +{ + if NetworkUpgrade::current(network, candidate_height) != NetworkUpgrade::NuTachyon { + return Vec::new(); + } + + let Some(source_height) = candidate_height + .0 + .checked_sub(MIN_TRANSPARENT_COINBASE_MATURITY) + .map(Height) + .filter(|height| *height > Height::MIN) + else { + return Vec::new(); + }; + + let block_request = read_state + .clone() + .oneshot(ReadRequest::Block(source_height.into())); + let mining_request = read_state.oneshot(ReadRequest::TachyonMiningData { + anchors: HashSet::new(), + tachygrams: HashSet::new(), + tip_hash, + candidate_height, + }); + + let responses = timeout(STATE_QUERY_TIMEOUT, async { + tokio::try_join!(block_request, mining_request) + }) + .await; + + let (source_block, tip_anchor) = match responses { + Ok(Ok(( + ReadResponse::Block(Some(source_block)), + ReadResponse::TachyonMiningData(Some(mining_data)), + ))) => (source_block, mining_data.tip_anchor), + Ok(Ok((_block, _mining_data))) => { + tracing::warn!(?source_height, "could not load Tachyon workload state"); + return Vec::new(); + } + Ok(Err(error)) => { + tracing::warn!(?error, "could not query Tachyon workload state"); + return Vec::new(); + } + Err(_) => { + tracing::warn!("timed out querying Tachyon workload state"); + return Vec::new(); + } + }; + + let Some(coinbase) = source_block.transactions.first() else { + tracing::warn!( + ?source_height, + "Tachyon workload source block has no coinbase" + ); + return Vec::new(); + }; + + let source_hash = coinbase.hash(); + let workload_script = workload_address(network).script(); + let mut transactions = Vec::with_capacity(TRANSACTIONS_PER_BLOCK); + + for (index, output) in coinbase.outputs().iter().enumerate() { + if output.lock_script != workload_script { + continue; + } + + let outpoint = transparent::OutPoint::from_usize(source_hash, index); + match build_transaction(candidate_height, outpoint, output.clone(), tip_anchor) { + Ok(transaction) => transactions.push(transaction), + Err(error) => { + tracing::warn!( + ?source_height, + index, + %error, + "could not build Tachyon workload transaction" + ); + return Vec::new(); + } + } + } + + if transactions.len() != TRANSACTIONS_PER_BLOCK { + tracing::warn!( + ?source_height, + expected = TRANSACTIONS_PER_BLOCK, + actual = transactions.len(), + "mature coinbase does not have the expected Tachyon workload outputs", + ); + return Vec::new(); + } + + transactions +} + +fn workload_address(network: &Network) -> transparent::Address { + transparent::Address::from_script_hash(network.t_addr_kind(), REDEEM_SCRIPT_HASH) +} + +fn build_transaction( + candidate_height: Height, + outpoint: transparent::OutPoint, + source_output: transparent::Output, + anchor: zakura_chain::tachyon::Anchor, +) -> Result { + let input_value = u64::try_from(source_output.value.zatoshis()) + .map_err(|error| format!("invalid source value: {error}"))?; + let output_value = input_value + .checked_sub(WORKLOAD_FEE) + .ok_or_else(|| "miner reward part is smaller than the workload fee".to_string())?; + + let mut seed: [u8; 32] = outpoint.hash.into(); + for (seed_byte, index_byte) in seed + .iter_mut() + .zip(outpoint.index.to_le_bytes().iter().cycle()) + { + *seed_byte ^= index_byte; + } + let mut rng = StdRng::from_seed(seed); + + let spending_key = private::SpendingKey::random(&mut rng); + let note = Note { + pk: spending_key.derive_payment_key(), + value: value::Positive::try_from(output_value) + .map_err(|error| format!("invalid Tachyon note value: {error}"))?, + psi: nullifier::Trapdoor::random(&mut rng), + rcm: CommitmentTrapdoor::random(&mut rng), + }; + let output_plan = action::Plan::output( + note, + ActionEntropy::random(&mut rng), + value::Trapdoor::random(&mut rng), + ); + let plan = bundle::Plan::new(Vec::new(), vec![output_plan]); + let proof_key = spending_key.derive_proof_private(); + let auth_key = spending_key.derive_auth_private(); + let anchor_bytes: [u8; 32] = anchor.into(); + let anchor = zcash_tachyon::Anchor::read(&anchor_bytes[..]) + .map_err(|error| format!("invalid stored Tachyon anchor: {error}"))?; + let stamp = plan + .clone() + .stamp_plan(anchor) + .prove(&mut rng, &proof_key, Vec::new()) + .map_err(|error| format!("could not create mock Ragu proof: {error}"))?; + + let placeholder_bundle = plan + .sign(&mut rng, &[0; 32], &auth_key) + .map_err(|error| format!("could not create placeholder signatures: {error}"))? + .stamp(stamp.clone()); + let placeholder = transaction( + candidate_height, + outpoint, + TachyonBundle::Proven(placeholder_bundle), + ); + let spent_outputs = Arc::new(vec![source_output]); + let sighash = placeholder + .sighash( + NetworkUpgrade::NuTachyon, + HashType::ALL, + spent_outputs.clone(), + None, + ) + .map_err(|error| format!("could not compute V7 sighash: {error}"))?; + + let bundle = plan + .sign(&mut rng, sighash.as_ref(), &auth_key) + .map_err(|error| format!("could not sign Tachyon bundle: {error}"))? + .stamp(stamp); + let transaction = transaction(candidate_height, outpoint, TachyonBundle::Proven(bundle)); + let fee = Amount::try_from(WORKLOAD_FEE) + .map_err(|error| format!("invalid Tachyon workload fee: {error}"))?; + + VerifiedUnminedTx::new(UnminedTx::from(transaction), fee, 0, 0, spent_outputs) + .map_err(|error| format!("invalid generated transaction: {error}")) +} + +fn transaction( + candidate_height: Height, + outpoint: transparent::OutPoint, + bundle: TachyonBundle, +) -> Transaction { + Transaction::V7 { + network_upgrade: NetworkUpgrade::NuTachyon, + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: candidate_height, + inputs: vec![transparent::Input::PrevOut { + outpoint, + // Push the one-byte OP_TRUE redeem script onto the P2SH script stack. + unlock_script: transparent::Script::new(&[1, OP_TRUE]), + sequence: u32::MAX, + }], + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + ironwood_shielded_data: None, + tachyon_shielded_data: Some(bundle.into()), + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use zakura_chain::{ + amount::NonNegative, + parameters::testnet::ConfiguredActivationHeights, + serialization::{ZcashDeserialize, ZcashDeserializeInto}, + transaction::Hash, + }; + use zakura_node_services::BoxError; + use zakura_state::TachyonMiningData; + use zakura_test::mock_service::MockService; + + use super::*; + + #[test] + fn generated_transaction_has_a_valid_tachyon_bundle() { + let network = Network::new_regtest(Default::default()); + let source_output = transparent::Output::new( + Amount::::try_from(100_000_000u64).expect("test value is valid"), + workload_address(&network).script(), + ); + let outpoint = transparent::OutPoint { + hash: Hash([7; 32]), + index: 2, + }; + + let generated = build_transaction( + Height(200), + outpoint, + source_output, + zakura_chain::tachyon::Anchor::default(), + ) + .expect("output-only mock proof can be generated"); + let transaction = generated.transaction.transaction(); + let TachyonBundle::Proven(bundle) = &transaction + .tachyon_shielded_data() + .expect("generated transaction has Tachyon data") + .0 + else { + panic!("generated transaction must carry its own proof"); + }; + + assert!(bundle.is_autonome()); + assert_eq!(transaction.outputs().len(), 0); + assert_eq!(transaction.inputs().len(), 1); + assert_eq!( + generated.miner_fee, + Amount::::try_from(WORKLOAD_FEE).unwrap() + ); + bundle + .verify_coverage(&[]) + .expect("proof stamp covers the generated action"); + } + + #[tokio::test] + async fn mature_reward_generates_and_aggregates_three_transactions() { + let network = Network::new_regtest( + ConfiguredActivationHeights { + canopy: Some(1), + nu5: Some(2), + nu6: Some(3), + nu6_1: Some(4), + nu6_2: Some(5), + nu6_3: Some(6), + nu7: Some(8), + nu_tachyon: Some(10), + ..Default::default() + } + .into(), + ); + let candidate_height = Height(200); + let source_height = Height(candidate_height.0 - MIN_TRANSPARENT_COINBASE_MATURITY); + let miner_params = super::super::MinerParams::new( + &network, + crate::config::mining::Config { + internal_miner: true, + tachyon_workload: true, + ..Default::default() + }, + ) + .expect("test network supports the workload"); + let coinbase = crate::methods::types::transaction::TransactionTemplate::new_coinbase( + &network, + source_height, + &miner_params, + Amount::zero(), + ) + .expect("workload coinbase can be built"); + let coinbase = coinbase + .data + .as_ref() + .zcash_deserialize_into() + .expect("workload coinbase deserializes"); + let mut source_block = + block::Block::zcash_deserialize(&zakura_test::vectors::BLOCK_MAINNET_GENESIS_BYTES[..]) + .expect("hardcoded genesis block deserializes"); + source_block.transactions = vec![Arc::new(coinbase)]; + let tip_hash = source_block.hash(); + let tip_anchor = zakura_chain::tachyon::Anchor::default(); + + let mut read_state: MockService<_, _, _, BoxError> = MockService::build().for_unit_tests(); + let generation = + generate_transactions(&network, candidate_height, tip_hash, read_state.clone()); + let responses = async { + read_state + .expect_request(ReadRequest::Block(source_height.into())) + .await + .respond(ReadResponse::Block(Some(Arc::new(source_block)))); + read_state + .expect_request(ReadRequest::TachyonMiningData { + anchors: HashSet::new(), + tachygrams: HashSet::new(), + tip_hash, + candidate_height, + }) + .await + .respond(ReadResponse::TachyonMiningData(Some(TachyonMiningData { + tip_anchor, + anchor_heights: HashMap::new(), + blocks: Default::default(), + revealed_tachygrams: HashSet::new(), + }))); + }; + let (generated, ()) = tokio::join!(generation, responses); + + assert_eq!(generated.len(), TRANSACTIONS_PER_BLOCK); + assert!(generated.iter().all(|transaction| matches!( + &transaction + .transaction + .transaction() + .tachyon_shielded_data() + .expect("generated transaction has Tachyon data") + .0, + TachyonBundle::Proven(bundle) if bundle.is_autonome() + ))); + + let anchor_heights = HashMap::from([(tip_anchor, Height(candidate_height.0 - 1))]); + let aggregation = super::super::tachyon::aggregate_transactions( + network, + candidate_height, + tip_hash, + read_state.clone(), + generated, + ); + let aggregation_response = async { + read_state + .expect_request_that(|request| { + matches!(request, ReadRequest::TachyonMiningData { .. }) + }) + .await + .respond(ReadResponse::TachyonMiningData(Some(TachyonMiningData { + tip_anchor, + anchor_heights, + blocks: Default::default(), + revealed_tachygrams: HashSet::new(), + }))); + }; + let (aggregated, ()) = tokio::join!(aggregation, aggregation_response); + + assert_eq!(aggregated.len(), TRANSACTIONS_PER_BLOCK); + assert!(matches!( + &aggregated[0] + .transaction + .transaction() + .tachyon_shielded_data() + .expect("aggregate has Tachyon data") + .0, + TachyonBundle::Proven(bundle) if bundle.is_aggregate() + )); + assert!(aggregated[1..].iter().all(|transaction| matches!( + transaction + .transaction + .transaction() + .tachyon_shielded_data() + .expect("adjunct has Tachyon data") + .0, + TachyonBundle::Adjunct(_) + ))); + } +} diff --git a/crates/zakura-rpc/src/methods/types/get_block_template/tests.rs b/crates/zakura-rpc/src/methods/types/get_block_template/tests.rs index 307455e43f..081d1e23ef 100644 --- a/crates/zakura-rpc/src/methods/types/get_block_template/tests.rs +++ b/crates/zakura-rpc/src/methods/types/get_block_template/tests.rs @@ -126,6 +126,130 @@ fn local_genesis_activation_coinbase_includes_lockbox_marker() -> anyhow::Result Ok(()) } +/// The Tachyon workload is limited to internal mining on proof-of-work-disabled test networks and +/// splits the miner reward into independently spendable outputs. +#[cfg(zcash_unstable = "nutachyon")] +#[test] +fn tachyon_workload_coinbase_outputs() { + use crate::{ + config::mining::Config, + methods::types::get_block_template::{REDEEM_SCRIPT_HASH, TRANSACTIONS_PER_BLOCK}, + }; + + let network = Network::new_regtest(Default::default()); + let config = Config { + internal_miner: true, + tachyon_workload: true, + ..Default::default() + }; + let miner_params = MinerParams::new(&network, config.clone()) + .expect("proof-of-work-disabled test networks support the Tachyon workload"); + let height = Height(20); + let template = + TransactionTemplate::new_coinbase(&network, height, &miner_params, Amount::zero()) + .expect("workload coinbase can be built"); + let coinbase: Transaction = template + .data() + .as_ref() + .zcash_deserialize_into() + .expect("workload coinbase deserializes"); + let workload_script = + transparent::Address::from_script_hash(network.t_addr_kind(), REDEEM_SCRIPT_HASH).script(); + + assert_eq!( + coinbase + .outputs() + .iter() + .filter(|output| output.lock_script == workload_script) + .count(), + TRANSACTIONS_PER_BLOCK, + ); + + let mut missing_internal_miner = config; + missing_internal_miner.internal_miner = false; + assert!(MinerParams::new(&network, missing_internal_miner).is_err()); + assert!(MinerParams::new( + &Network::Mainnet, + Config { + internal_miner: true, + tachyon_workload: true, + ..Default::default() + } + ) + .is_err()); +} + +/// A NuTachyon template must convert into a mineable proposal block. +#[cfg(zcash_unstable = "nutachyon")] +#[test] +fn nu_tachyon_template_converts_to_proposal_block() -> anyhow::Result<()> { + use crate::methods::types::{ + get_block_template::proposal::proposal_block_from_template, long_poll::LongPollInput, + }; + use zakura_chain::{ + block::ChainHistoryMmrRootHash, + serialization::{BytesInDisplayOrder, DateTime32}, + work::difficulty::{CompactDifficulty, ExpandedDifficulty, U256}, + }; + + let net = testnet::Parameters::build() + .with_activation_heights(ConfiguredActivationHeights { + canopy: Some(1), + nu5: Some(2), + nu6: Some(3), + nu6_1: Some(4), + nu6_2: Some(5), + nu6_3: Some(6), + nu_tachyon: Some(7), + ..Default::default() + })? + .clear_funding_streams() + .to_network()?; + let height = NetworkUpgrade::NuTachyon + .activation_height(&net) + .ok_or(anyhow!("NuTachyon activation height must be configured"))?; + let tip_height = height.previous()?; + let miner_params = MinerParams::from( + Address::decode( + &net, + default_miner_address(net.kind(), &MinerAddressType::Transparent), + ) + .ok_or(anyhow!("hard-coded transparent address must be valid"))?, + ); + let now = DateTime32::now(); + let chain_info = zakura_state::GetBlockTemplateChainInfo { + tip_hash: net.genesis_hash(), + tip_height, + chain_history_root: Some(ChainHistoryMmrRootHash::default()), + expected_difficulty: CompactDifficulty::from(ExpandedDifficulty::from(U256::one())), + cur_time: now, + min_time: now, + max_time: now, + }; + let long_poll_id = LongPollInput::new(tip_height, chain_info.tip_hash, now, []).generate_id(); + let template = super::BlockTemplateResponse::new_internal( + &net, + None, + &miner_params, + &chain_info, + long_poll_id, + vec![], + None, + ); + + let block = proposal_block_from_template(&template, None, &net)?; + + assert_eq!( + block.header.commitment_bytes.0, + template + .default_roots + .block_commitments_hash + .bytes_in_serialized_order(), + ); + + Ok(()) +} + /// The Zakura marker is always prepended, and `extra_coinbase_data` can't exceed /// the limit. #[test] diff --git a/crates/zakura-rpc/src/methods/types/get_blockchain_info.rs b/crates/zakura-rpc/src/methods/types/get_blockchain_info.rs index 227e76b47d..9c351aafc7 100644 --- a/crates/zakura-rpc/src/methods/types/get_blockchain_info.rs +++ b/crates/zakura-rpc/src/methods/types/get_blockchain_info.rs @@ -9,6 +9,8 @@ use zakura_chain::{ use zec::Zec; +use crate::methods::BlockchainValuePoolBalances; + use super::*; /// A value pool's balance in Zec and Zatoshis @@ -37,9 +39,8 @@ pub struct GetBlockchainInfoBalance { } impl GetBlockchainInfoBalance { - /// Returns zero-value transparent, Sprout, Sapling, Orchard, Lockbox, and - /// Ironwood value pools. - pub fn zero_pools() -> [Self; 6] { + /// Returns zero-value entries for every tracked value pool. + pub fn zero_pools() -> BlockchainValuePoolBalances { Self::value_pools(Default::default(), None) } @@ -88,18 +89,22 @@ impl GetBlockchainInfoBalance { Self::new_internal("ironwood", amount, delta) } + /// Creates a [`GetBlockchainInfoBalance`] for the Tachyon pool. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon(amount: Amount, delta: Option>) -> Self { + Self::new_internal("tachyon", amount, delta) + } + /// Creates a [`GetBlockchainInfoBalance`] for the Lockbox pool. pub fn deferred(amount: Amount, delta: Option>) -> Self { Self::new_internal("lockbox", amount, delta) } - /// Converts a [`ValueBalance`] to transparent, Sprout, Sapling, Orchard, - /// Lockbox, and Ironwood value pool entries, keeping the pre-Ironwood pool - /// indexes stable. + /// Converts a [`ValueBalance`] to RPC value pool entries in activation order. pub fn value_pools( value_balance: ValueBalance, delta_balance: Option>, - ) -> [Self; 6] { + ) -> BlockchainValuePoolBalances { [ Self::transparent( value_balance.transparent_amount(), @@ -125,6 +130,11 @@ impl GetBlockchainInfoBalance { value_balance.ironwood_amount(), delta_balance.map(|b| b.ironwood_amount()), ), + #[cfg(zcash_unstable = "nutachyon")] + Self::tachyon( + value_balance.tachyon_amount(), + delta_balance.map(|b| b.tachyon_amount()), + ), ] } @@ -143,3 +153,40 @@ impl GetBlockchainInfoBalance { .expect("at least one pool") } } + +#[cfg(all(test, zcash_unstable = "nutachyon"))] +mod tests { + use super::*; + + #[test] + fn tachyon_pool_is_included_in_rpc_balances_and_chain_supply() { + let tachyon_amount = Amount::try_from(123_456_u64).expect("test amount is valid"); + let value_balance = ValueBalance::from_tachyon_amount(tachyon_amount); + + let pools = GetBlockchainInfoBalance::value_pools(value_balance, None); + + assert_eq!(pools.len(), 7); + assert_eq!(pools[6].id().as_str(), "tachyon"); + assert_eq!(pools[6].chain_value_zat(), tachyon_amount); + assert_eq!( + GetBlockchainInfoBalance::chain_supply(value_balance).chain_value_zat(), + tachyon_amount, + ); + } + + #[test] + fn legacy_rpc_balances_decode_with_an_empty_tachyon_pool() { + let legacy_pools = GetBlockchainInfoBalance::value_pools(ValueBalance::zero(), None) + .into_iter() + .take(6) + .collect(); + + let pools = crate::methods::blockchain_value_pool_balances_from_vec::< + serde::de::value::Error, + >(legacy_pools) + .expect("six-pool RPC balances remain compatible"); + + assert_eq!(pools[6].id().as_str(), "tachyon"); + assert_eq!(pools[6].chain_value_zat(), Amount::::zero()); + } +} diff --git a/crates/zakura-rpc/src/methods/types/transaction.rs b/crates/zakura-rpc/src/methods/types/transaction.rs index 7fc8dd9bc4..a2209b0ac8 100644 --- a/crates/zakura-rpc/src/methods/types/transaction.rs +++ b/crates/zakura-rpc/src/methods/types/transaction.rs @@ -220,6 +220,28 @@ impl TransactionTemplate { }; let add_transparent_reward = |builder: &mut Builder<_, _>, addr| { + #[cfg(zcash_unstable = "nutachyon")] + if miner_params.tachyon_workload() { + let output_count = super::get_block_template::TRANSACTIONS_PER_BLOCK; + let output_count_u64 = + u64::try_from(output_count).expect("the fixed workload count fits in u64"); + let total = miner_reward.into_u64(); + let quotient = total / output_count_u64; + let remainder = total % output_count_u64; + + return (0..output_count) + .try_for_each(|index| { + let value = quotient + u64::from(index == 0) * remainder; + builder.add_transparent_output( + addr, + Zatoshis::from_u64(value) + .expect("parts of a valid miner reward remain valid"), + ) + }) + .map_err(|err| tracing::error!("Failed to add transparent output: {err}")) + .ok(); + } + trace_err!( builder.add_transparent_output(addr, miner_reward), "transparent" diff --git a/crates/zakura-rpc/tests/serialization_tests.rs b/crates/zakura-rpc/tests/serialization_tests.rs index 2d9b89bee0..0e32b44b52 100644 --- a/crates/zakura-rpc/tests/serialization_tests.rs +++ b/crates/zakura-rpc/tests/serialization_tests.rs @@ -236,12 +236,22 @@ fn test_get_blockchain_info_accepts_legacy_value_pools() -> Result<(), Box Result<(), Box Result<(), Box), + #[error("error building the history tree: {0}")] HistoryTreeError(#[from] Arc), @@ -820,6 +824,31 @@ pub enum ValidateContextError { tx_index_in_block: Option, transaction_hash: transaction::Hash, }, + + #[error("unknown Tachyon anchor {anchor:?}")] + #[cfg(zcash_unstable = "nutachyon")] + #[non_exhaustive] + UnknownTachyonAnchor { + anchor: zakura_chain::tachyon::Anchor, + height: Option, + tx_index_in_block: Option, + transaction_hash: transaction::Hash, + }, + + #[error("duplicate Tachyon Tachygram {tachygram:?}, in finalized state: {in_finalized_state}")] + #[cfg(zcash_unstable = "nutachyon")] + #[non_exhaustive] + DuplicateTachyonTachygram { + tachygram: zakura_chain::tachyon::Tachygram, + in_finalized_state: bool, + }, +} + +#[cfg(zcash_unstable = "nutachyon")] +impl From for ValidateContextError { + fn from(error: zcash_tachyon::AnchorError) -> Self { + Self::TachyonAnchorError(Arc::new(error)) + } } impl ValidateContextError { @@ -853,6 +882,10 @@ impl ValidateContextError { | Self::HistoryTreeError(_) => { BodyVerificationClass::Retryable(TransientBodyFailureKind::Storage) } + #[cfg(zcash_unstable = "nutachyon")] + Self::TachyonAnchorError(_) => { + BodyVerificationClass::Retryable(TransientBodyFailureKind::Storage) + } Self::InvalidBlockCommitment(error) => { let kind = match error { CommitmentError::InvalidAuthDataRoot { .. } => BodyCommitmentKind::AuthDataRoot, @@ -942,6 +975,12 @@ impl ValidateContextError { Self::UnknownSaplingAnchor { .. } => consensus("context.unknown_sapling_anchor"), Self::UnknownOrchardAnchor { .. } => consensus("context.unknown_orchard_anchor"), Self::UnknownIronwoodAnchor { .. } => consensus("context.unknown_ironwood_anchor"), + #[cfg(zcash_unstable = "nutachyon")] + Self::UnknownTachyonAnchor { .. } => consensus("context.unknown_tachyon_anchor"), + #[cfg(zcash_unstable = "nutachyon")] + Self::DuplicateTachyonTachygram { .. } => { + consensus("context.duplicate_tachyon_tachygram") + } } } @@ -971,6 +1010,10 @@ impl ValidateContextError { | ValidateContextError::UnknownSaplingAnchor { .. } | ValidateContextError::UnknownOrchardAnchor { .. } | ValidateContextError::UnknownIronwoodAnchor { .. } => 100, + #[cfg(zcash_unstable = "nutachyon")] + ValidateContextError::DuplicateTachyonTachygram { .. } => 100, + #[cfg(zcash_unstable = "nutachyon")] + ValidateContextError::UnknownTachyonAnchor { .. } => 100, // Residual arithmetic failures in our own value summation, not // consensus violations. `remaining_transaction_value()` and @@ -1001,6 +1044,8 @@ impl ValidateContextError { | ValidateContextError::OrphanedBlock { .. } | ValidateContextError::NoteCommitmentTreeError(_) | ValidateContextError::HistoryTreeError(_) => 0, + #[cfg(zcash_unstable = "nutachyon")] + ValidateContextError::TachyonAnchorError(_) => 0, } } diff --git a/crates/zakura-state/src/lib.rs b/crates/zakura-state/src/lib.rs index 3210385ce8..d30177183f 100644 --- a/crates/zakura-state/src/lib.rs +++ b/crates/zakura-state/src/lib.rs @@ -64,6 +64,8 @@ pub use request::{ #[cfg(feature = "indexer")] pub use request::Spend; +#[cfg(zcash_unstable = "nutachyon")] +pub use response::TachyonMiningData; pub use response::{ AnyTx, BlockSyncBodyMetadata, GetBlockTemplateChainInfo, KnownBlock, MinedTx, NonFinalizedBlocksListener, ReadResponse, Response, diff --git a/crates/zakura-state/src/request.rs b/crates/zakura-state/src/request.rs index 9f699e4a65..2728582331 100644 --- a/crates/zakura-state/src/request.rs +++ b/crates/zakura-state/src/request.rs @@ -11,6 +11,8 @@ use std::{ }; use tower::{BoxError, Service, ServiceExt}; +#[cfg(zcash_unstable = "nutachyon")] +use zakura_chain::tachyon; use zakura_chain::{ amount::{DeferredPoolBalanceChange, NegativeAllowed}, block::{ @@ -370,6 +372,10 @@ impl Treestate { sapling_subtree: Option>, orchard_subtree: Option>, ironwood_subtree: Option>, + #[cfg(zcash_unstable = "nutachyon")] tachyon_anchor: zakura_chain::tachyon::Anchor, + #[cfg(zcash_unstable = "nutachyon")] tachyon_epoch_anchor: Option< + zakura_chain::tachyon::Anchor, + >, history_tree: Arc, ) -> Self { Self { @@ -381,6 +387,10 @@ impl Treestate { orchard_subtree, ironwood, ironwood_subtree, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_epoch_anchor, }, history_tree, } @@ -1463,6 +1473,25 @@ pub enum ReadRequest { /// with the pool values of the current best chain tip. TipPoolValues, + /// Returns the chain data needed to aggregate transactions rooted at Tachyon anchors. + /// + /// Returns [`ReadResponse::TachyonMiningData`] if `tip_hash` is still the current best-chain + /// tip. Unknown anchors are omitted. For every epoch represented by a known anchor, the + /// response also contains the current tip anchor, the blocks between the earliest and latest + /// requested anchors, and any requested tachygrams already revealed within the candidate's + /// two-epoch window. + #[cfg(zcash_unstable = "nutachyon")] + TachyonMiningData { + /// Anchors referenced by the selected autonome transactions. + anchors: HashSet, + /// Tachygrams revealed by the selected autonome transactions. + tachygrams: HashSet, + /// The chain tip on which the block template is being built. + tip_hash: block::Hash, + /// The height of the candidate block being built on `tip_hash`. + candidate_height: block::Height, + }, + /// Looks up the block info after a block by hash or height in the current best chain. /// /// * [`ReadResponse::BlockInfo(Some(pool_values))`](ReadResponse::BlockInfo) if the block is in the best chain; @@ -1939,6 +1968,8 @@ impl ReadRequest { ReadRequest::Tip => "tip", ReadRequest::FinalizedTip => "finalized_tip", ReadRequest::TipPoolValues => "tip_pool_values", + #[cfg(zcash_unstable = "nutachyon")] + ReadRequest::TachyonMiningData { .. } => "tachyon_mining_data", ReadRequest::BlockInfo(_) => "block_info", ReadRequest::Depth(_) => "depth", ReadRequest::Block(_) => "block", diff --git a/crates/zakura-state/src/response.rs b/crates/zakura-state/src/response.rs index 4293db3d48..2578f6d1e1 100644 --- a/crates/zakura-state/src/response.rs +++ b/crates/zakura-state/src/response.rs @@ -7,6 +7,10 @@ use std::{ use chrono::{DateTime, Utc}; +#[cfg(zcash_unstable = "nutachyon")] +use std::collections::HashMap; +#[cfg(zcash_unstable = "nutachyon")] +use zakura_chain::tachyon; use zakura_chain::{ amount::{Amount, NonNegative}, block::{self, Block, ChainHistoryMmrRootHash}, @@ -35,6 +39,23 @@ use crate::{ #[cfg(test)] mod tests; +/// Best-chain data used to aggregate selected autonome Tachyon transactions. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg(zcash_unstable = "nutachyon")] +pub struct TachyonMiningData { + /// The Tachyon anchor at the current best-chain tip. + pub tip_anchor: tachyon::Anchor, + + /// Heights that created the requested anchors. Unknown anchors are omitted. + pub anchor_heights: HashMap, + + /// Blocks needed to advance from each epoch's earliest requested anchor to its latest one. + pub blocks: BTreeMap>, + + /// Requested tachygrams already revealed in the candidate block's two-epoch window. + pub revealed_tachygrams: HashSet, +} + #[derive(Clone, Debug, PartialEq, Eq)] /// A response to a [`StateService`](crate::service::StateService) [`Request`]. pub enum Response { @@ -427,6 +448,11 @@ pub enum ReadResponse { value_balance: ValueBalance, }, + /// Response to [`ReadRequest::TachyonMiningData`], or `None` when the requested chain tip is + /// no longer current. + #[cfg(zcash_unstable = "nutachyon")] + TachyonMiningData(Option), + /// Response to [`ReadRequest::BlockInfo`] with /// the block info after the specified block. BlockInfo(Option), @@ -729,6 +755,11 @@ impl TryFrom for Response { Err("there is no corresponding Response for this ReadResponse") } + #[cfg(zcash_unstable = "nutachyon")] + ReadResponse::TachyonMiningData(_) => { + Err("there is no corresponding Response for this ReadResponse") + } + #[cfg(feature = "indexer")] ReadResponse::TransactionId(_) | ReadResponse::RawBlocks(_) => { Err("there is no corresponding Response for this ReadResponse") diff --git a/crates/zakura-state/src/service.rs b/crates/zakura-state/src/service.rs index 862677446e..6cbaf4c6ef 100644 --- a/crates/zakura-state/src/service.rs +++ b/crates/zakura-state/src/service.rs @@ -2499,6 +2499,103 @@ impl Service for ReadStateService { }) } + #[cfg(zcash_unstable = "nutachyon")] + ReadRequest::TachyonMiningData { + anchors, + tachygrams, + tip_hash, + candidate_height, + } => { + let best_chain = state.latest_best_chain(); + if read::tip(best_chain.clone(), &state.db).is_none_or( + |(current_tip_height, current_tip_hash)| { + current_tip_hash != tip_hash + || current_tip_height.0.checked_add(1) != Some(candidate_height.0) + }, + ) { + return Ok(ReadResponse::TachyonMiningData(None)); + } + + let anchor_heights: HashMap<_, _> = anchors + .into_iter() + .filter_map(|anchor| { + best_chain + .as_ref() + .and_then(|chain| chain.tachyon_anchors.get(&anchor)) + .and_then(|heights| heights.last().copied()) + .or_else(|| state.db.tachyon_anchor_height(&anchor)) + .map(|height| (anchor, height)) + }) + .collect(); + + let tip_anchor = best_chain.as_ref().map_or_else( + || state.db.tachyon_anchor_for_tip(), + |chain| chain.tachyon_anchor_for_tip(), + ); + + let mut epoch_ranges = HashMap::new(); + for &height in anchor_heights.values() { + let Some(epoch) = zakura_chain::tachyon::epoch(&state.network, height) else { + continue; + }; + + epoch_ranges + .entry(epoch) + .and_modify(|(start, end): &mut (block::Height, block::Height)| { + *start = (*start).min(height); + *end = (*end).max(height); + }) + .or_insert((height, height)); + } + + let mut blocks = BTreeMap::new(); + for (start, end) in epoch_ranges.into_values() { + if start == end { + continue; + } + + for height_value in (start.0 + 1)..=end.0 { + let height = block::Height(height_value); + if let Some(block) = + read::block(best_chain.clone(), &state.db, height.into()) + { + blocks.insert(height, block); + } + } + } + + let revealed_tachygrams = tachygrams + .into_iter() + .filter(|tachygram| { + let in_window = |revealed_height| { + zakura_chain::tachyon::within_scan_window( + &state.network, + revealed_height, + candidate_height, + ) + }; + + best_chain + .as_ref() + .and_then(|chain| chain.tachyon_tachygrams.get(tachygram)) + .is_some_and(|heights| heights.iter().copied().any(in_window)) + || state + .db + .tachyon_tachygram_revealed_height(tachygram) + .is_some_and(in_window) + }) + .collect(); + + Ok(ReadResponse::TachyonMiningData(Some( + crate::response::TachyonMiningData { + tip_anchor, + anchor_heights, + blocks, + revealed_tachygrams, + }, + ))) + } + // Used by getblock ReadRequest::BlockInfo(hash_or_height) => Ok(ReadResponse::BlockInfo( read::block_info(state.latest_best_chain(), &state.db, hash_or_height), diff --git a/crates/zakura-state/src/service/arbitrary.rs b/crates/zakura-state/src/service/arbitrary.rs index cae90342fe..721ac8a4e1 100644 --- a/crates/zakura-state/src/service/arbitrary.rs +++ b/crates/zakura-state/src/service/arbitrary.rs @@ -175,6 +175,8 @@ impl Strategy for PreparedChain { &Default::default(), &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("history tree should be created"); *chain = Some(( diff --git a/crates/zakura-state/src/service/check/anchors.rs b/crates/zakura-state/src/service/check/anchors.rs index 3e39fd8eb0..7954bc0522 100644 --- a/crates/zakura-state/src/service/check/anchors.rs +++ b/crates/zakura-state/src/service/check/anchors.rs @@ -154,6 +154,39 @@ fn sapling_orchard_ironwood_anchors_refer_to_final_treestates( ); } + #[cfg(zcash_unstable = "nutachyon")] + if let Some(tachyon_shielded_data) = transaction.tachyon_shielded_data() { + if let zcash_tachyon::TachyonBundle::Proven(bundle) = &tachyon_shielded_data.0 { + let anchor = zakura_chain::tachyon::Anchor::from(bundle.stamp.anchor); + let network = finalized_state.network(); + let anchor_in_window = |anchor_height: Height| match height { + Some(height) => { + zakura_chain::tachyon::within_scan_window(&network, anchor_height, height) + } + None => true, + }; + let in_parent_chain = parent_chain.is_some_and(|chain| { + chain + .tachyon_anchors + .get(&anchor) + .is_some_and(|heights| heights.iter().copied().any(anchor_in_window)) + }); + + if !in_parent_chain + && !finalized_state + .tachyon_anchor_height(&anchor) + .is_some_and(anchor_in_window) + { + return Err(ValidateContextError::UnknownTachyonAnchor { + anchor, + height, + tx_index_in_block, + transaction_hash, + }); + } + } + } + Ok(()) } diff --git a/crates/zakura-state/src/service/check/nullifier.rs b/crates/zakura-state/src/service/check/nullifier.rs index c8914cb3ac..0fe34f5729 100644 --- a/crates/zakura-state/src/service/check/nullifier.rs +++ b/crates/zakura-state/src/service/check/nullifier.rs @@ -62,6 +62,29 @@ pub(crate) fn no_duplicates_in_finalized_chain( } } + #[cfg(zcash_unstable = "nutachyon")] + let network = finalized_state.network(); + #[cfg(zcash_unstable = "nutachyon")] + for transaction in &semantically_verified.block.transactions { + for tachygram in transaction.tachyon_tachygrams() { + if finalized_state + .tachyon_tachygram_revealed_height(&tachygram) + .is_some_and(|revealed_height| { + zakura_chain::tachyon::within_scan_window( + &network, + revealed_height, + semantically_verified.height, + ) + }) + { + return Err(ValidateContextError::DuplicateTachyonTachygram { + tachygram, + in_finalized_state: true, + }); + } + } + } + Ok(()) } diff --git a/crates/zakura-state/src/service/check/tests/vectors.rs b/crates/zakura-state/src/service/check/tests/vectors.rs index 59b871ddd7..9293d1d266 100644 --- a/crates/zakura-state/src/service/check/tests/vectors.rs +++ b/crates/zakura-state/src/service/check/tests/vectors.rs @@ -89,6 +89,8 @@ fn block_commitment_uses_the_precomputed_auth_data_root() { &sapling_root, &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("NU5 parent builds a history tree"); diff --git a/crates/zakura-state/src/service/finalized_state.rs b/crates/zakura-state/src/service/finalized_state.rs index 6fafed123d..00822e4d26 100644 --- a/crates/zakura-state/src/service/finalized_state.rs +++ b/crates/zakura-state/src/service/finalized_state.rs @@ -214,6 +214,14 @@ pub const STATE_COLUMN_FAMILIES_IN_CODE: &[&str] = &[ "ironwood_anchors", "ironwood_note_commitment_tree", "ironwood_note_commitment_subtree", + #[cfg(zcash_unstable = "nutachyon")] + "tachyon_anchors", + #[cfg(zcash_unstable = "nutachyon")] + "tachyon_anchor_by_height", + #[cfg(zcash_unstable = "nutachyon")] + "tachyon_epoch_anchor_by_epoch", + #[cfg(zcash_unstable = "nutachyon")] + "tachyon_tachygrams", // Chain "history_tree", "tip_chain_value_pool", @@ -897,6 +905,13 @@ impl FinalizedState { }), }; + // The roots-only VCT payload does not carry Tachyon anchors or transaction + // counts, so NuTachyon blocks must use the body-derived commitment path. + #[cfg(zcash_unstable = "nutachyon")] + let vct_roots = vct_roots.filter(|_| { + NetworkUpgrade::current(&network, height) < NetworkUpgrade::NuTachyon + }); + let mut vct_write = VctWriteData::default(); if let Some((sapling_root, orchard_root, ironwood_root)) = vct_roots { @@ -1028,6 +1043,8 @@ impl FinalizedState { commitment_aux_verify::verify_commitment_roots( &network, (*history_tree).clone(), + #[cfg(zcash_unstable = "nutachyon")] + prev_note_commitment_trees.tachyon_anchor, verification_items, ) }) @@ -1171,6 +1188,10 @@ impl FinalizedState { orchard_subtree: None, ironwood: ironwood_frontier, ironwood_subtree: None, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: note_commitment_trees.tachyon_anchor, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_epoch_anchor: None, }; // The handoff writes the real final frontier as the tip @@ -1256,10 +1277,24 @@ impl FinalizedState { commitment_result.expect("scope has already finished")?; // Update the history tree (depends on both operations above). + #[cfg(zcash_unstable = "nutachyon")] + if let Some(pool_height) = + zakura_chain::tachyon::pool_height(&network, checkpoint_verified.height) + { + let advance = note_commitment_trees + .tachyon_anchor + .advance_with_block(pool_height, &block) + .map_err(ValidateContextError::from)?; + note_commitment_trees.tachyon_anchor = advance.post_block; + note_commitment_trees.tachyon_epoch_anchor = advance.epoch_boundary; + } + let history_tree_mut = Arc::make_mut(&mut history_tree); let sapling_root = note_commitment_trees.sapling.root(); let orchard_root = note_commitment_trees.orchard.root(); let ironwood_root = note_commitment_trees.ironwood.root(); + #[cfg(zcash_unstable = "nutachyon")] + let tachyon_anchor = note_commitment_trees.tachyon_anchor; history_tree_mut .push( &network, @@ -1267,6 +1302,8 @@ impl FinalizedState { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &tachyon_anchor, ) .map_err(Arc::new) .map_err(ValidateContextError::from)?; diff --git a/crates/zakura-state/src/service/finalized_state/commitment_aux_verify.rs b/crates/zakura-state/src/service/finalized_state/commitment_aux_verify.rs index 0a75849788..354163c331 100644 --- a/crates/zakura-state/src/service/finalized_state/commitment_aux_verify.rs +++ b/crates/zakura-state/src/service/finalized_state/commitment_aux_verify.rs @@ -246,6 +246,7 @@ pub(crate) fn verify_supplied_ironwood_root_below_nu6_3( pub(crate) fn verify_commitment_roots( network: &Network, mut history_tree: HistoryTree, + #[cfg(zcash_unstable = "nutachyon")] mut tachyon_anchor: zakura_chain::tachyon::Anchor, blocks_to_verify: I, ) -> Result where @@ -295,6 +296,14 @@ where auth_data_root, ) .map_err(|error| match error { + #[cfg(zcash_unstable = "nutachyon")] + SuppliedRootsError::TachyonDataUnavailable => { + ValidateContextError::HistoryTreeError(Arc::new( + zakura_chain::history_tree::HistoryTreeError::InvalidCachedTree { + reason: "Tachyon history commitments require block bodies", + }, + )) + } SuppliedRootsError::InvalidHeaderCommitment(error) => { ValidateContextError::InvalidBlockCommitment(error) } @@ -328,10 +337,27 @@ where verify_supplied_ironwood_root_below_nu6_3(network, height, &ironwood_root) .map_err(|error| CommitmentRootVerificationError::CurrentRoots { height, error })?; + #[cfg(zcash_unstable = "nutachyon")] + if let Some(pool_height) = zakura_chain::tachyon::pool_height(network, height) { + tachyon_anchor = tachyon_anchor + .advance_with_block(pool_height, &block) + .map_err(ValidateContextError::from) + .map_err(|error| CommitmentRootVerificationError::CurrentRoots { height, error })? + .post_block; + } + // Fold this block's supplied roots into the running MMR (builds the leaf // from the block body tx-counts + the roots). history_tree - .push(network, block, &sapling_root, &orchard_root, &ironwood_root) + .push( + network, + block, + &sapling_root, + &orchard_root, + &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &tachyon_anchor, + ) .map_err(Arc::new) .map_err(ValidateContextError::from) .map_err(|error| CommitmentRootVerificationError::CurrentRoots { height, error })?; @@ -367,6 +393,8 @@ mod tests { &Default::default(), &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("empty history tree for a pre-Heartwood block") } @@ -702,8 +730,14 @@ mod tests { Some(next_block.auth_data_root()), ), ]; - verify_commitment_roots(&Mainnet, empty_history_tree(), ok_items) - .expect("real roots verify against the headers"); + verify_commitment_roots( + &Mainnet, + empty_history_tree(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), + ok_items, + ) + .expect("real roots verify against the headers"); // Negative + lag: a wrong root at the activation height (here, the next // block's root, which is a valid but different root) is only caught when the @@ -717,8 +751,14 @@ mod tests { Some(next_block.auth_data_root()), ), ]; - let failure = verify_commitment_roots(&Mainnet, empty_history_tree(), bad_items) - .expect_err("a wrong root must be rejected"); + let failure = verify_commitment_roots( + &Mainnet, + empty_history_tree(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), + bad_items, + ) + .expect_err("a wrong root must be rejected"); assert_eq!( failure.height().0, activation + 1, @@ -824,8 +864,14 @@ mod tests { let items: Vec<_> = (start..=end + 1).map(item_at).collect(); // Positive: every supplied root in the range is confirmed by the V2 headers. - verify_commitment_roots(&Mainnet, seed.clone(), items.clone()) - .expect("real NU5 roots verify against the headers"); + verify_commitment_roots( + &Mainnet, + seed.clone(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), + items.clone(), + ) + .expect("real NU5 roots verify against the headers"); eprintln!("VCT NU5 positive: {} blocks verified", items.len()); // Negative + lag: corrupt one root mid-range with a distinct valid root (the @@ -848,8 +894,14 @@ mod tests { .as_mut() .expect("test verification item has roots") .0 = wrong_root; - let failure = verify_commitment_roots(&Mainnet, seed, bad_items) - .expect_err("a wrong NU5 root must be rejected"); + let failure = verify_commitment_roots( + &Mainnet, + seed, + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), + bad_items, + ) + .expect_err("a wrong NU5 root must be rejected"); assert_eq!( failure.height().0, bad_height + 1, @@ -1060,15 +1112,20 @@ mod tests { .0; for &(start, end) in ranges.iter().filter(|(start, _)| *start < heartwood) { let items = (start..=end).map(|height| verification_item_at(Height(height))); - verify_commitment_roots(&Mainnet, empty_history_tree(), items).unwrap_or_else( - |failure| { - panic!( - "pre-Heartwood roots failed at {:?}: {}", - failure.height(), - failure.error() - ) - }, - ); + verify_commitment_roots( + &Mainnet, + empty_history_tree(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), + items, + ) + .unwrap_or_else(|failure| { + panic!( + "pre-Heartwood roots failed at {:?}: {}", + failure.height(), + failure.error() + ) + }); eprintln!("validated direct pre-Heartwood commitments for {start}..={end}"); } @@ -1104,6 +1161,8 @@ mod tests { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("network-upgrade activation starts a history-tree epoch"); let confirm_end = end @@ -1112,7 +1171,14 @@ mod tests { let items = (activation + 1..=confirm_end).map(|height| verification_item_at(Height(height))); - verify_commitment_roots(&Mainnet, history_tree, items).unwrap_or_else(|failure| { + verify_commitment_roots( + &Mainnet, + history_tree, + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), + items, + ) + .unwrap_or_else(|failure| { panic!( "{upgrade:?} MMR linkage failed at {:?} while validating through \ {confirm_end}: {}", diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/chain.rs b/crates/zakura-state/src/service/finalized_state/disk_format/chain.rs index 9c527c2314..00ee22145c 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/chain.rs +++ b/crates/zakura-state/src/service/finalized_state/disk_format/chain.rs @@ -26,14 +26,17 @@ use crate::service::finalized_state::disk_format::{FromDisk, IntoDisk}; /// An error decoding a persisted history-tree snapshot. #[derive(Debug, Error)] pub enum HistoryTreeDecodeError { - /// The snapshot does not use either supported serialization format. + /// The snapshot does not use any supported serialization format. #[error( - "history tree snapshot is neither the current nor legacy format: \ - current format error: {current}; legacy format error: {legacy}" + "history tree snapshot is neither the current, Ironwood, nor legacy format: \ + current format error: {current}; Ironwood format error: {ironwood}; \ + legacy format error: {legacy}" )] InvalidEncoding { /// The current-width decoding error. current: bincode::Error, + /// The Ironwood-width decoding error. + ironwood: bincode::Error, /// The legacy-width decoding error. legacy: bincode::Error, }, @@ -55,7 +58,10 @@ pub enum HistoryTreeDecodeError { } impl IntoDisk for ValueBalance { + #[cfg(not(zcash_unstable = "nutachyon"))] type Bytes = [u8; 48]; + #[cfg(zcash_unstable = "nutachyon")] + type Bytes = [u8; 56]; fn as_bytes(&self) -> Self::Bytes { self.to_bytes() @@ -89,19 +95,26 @@ impl HistoryTreeParts { let bytes = bytes.as_ref(); let options = bincode::DefaultOptions::new(); - // Try the current entry width first. Databases written before NU6.3 widened - // `zcash_history::Entry` store narrower entries that fail to parse at the current width, - // so fall back to the legacy width and zero-pad each entry up to the current width. + // Try the current entry width first. Earlier databases store narrower entries, so fall + // back through the Ironwood and pre-Ironwood widths and zero-pad each entry to the current + // width. // // Legacy-width rows can fail the current-width decoder with errors other than // `UnexpectedEof`, because the wider entry can read into the next legacy entry and // interpret arbitrary entry bytes as bincode control bytes. match options.deserialize::(bytes) { Ok(parts) => Ok(parts), - Err(current) => options - .deserialize::(bytes) - .map(HistoryTreeParts::from) - .map_err(|legacy| HistoryTreeDecodeError::InvalidEncoding { current, legacy }), + Err(current) => match options.deserialize::(bytes) { + Ok(parts) => Ok(parts.into()), + Err(ironwood) => options + .deserialize::(bytes) + .map(HistoryTreeParts::from) + .map_err(|legacy| HistoryTreeDecodeError::InvalidEncoding { + current, + ironwood, + legacy, + }), + }, } } @@ -152,6 +165,46 @@ impl IntoDisk for HistoryTreeParts { /// before NU6.3 widened `zcash_history::NodeData`. const LEGACY_MAX_ENTRY_SIZE: usize = 253; +/// The width of a history-tree [`zcash_history::Entry`] from NU6.3 until NuTachyon support +/// reserved space for V4 node data. +const IRONWOOD_MAX_ENTRY_SIZE: usize = 326; + +/// A mirror of [`HistoryTreeParts`] using the NU6.3 [`zcash_history::Entry`] width. +#[derive(serde::Serialize, serde::Deserialize)] +struct IronwoodHistoryTreeParts { + network_kind: NetworkKind, + size: u32, + peaks: BTreeMap, + current_height: Height, +} + +/// A history-tree entry serialized at the NU6.3 [`IRONWOOD_MAX_ENTRY_SIZE`] width. +#[derive(serde::Serialize, serde::Deserialize)] +struct IronwoodEntry { + #[serde(with = "BigArray")] + inner: [u8; IRONWOOD_MAX_ENTRY_SIZE], +} + +impl From for HistoryTreeParts { + fn from(ironwood: IronwoodHistoryTreeParts) -> Self { + HistoryTreeParts { + network_kind: ironwood.network_kind, + size: ironwood.size, + peaks: ironwood + .peaks + .into_iter() + .map(|(index, entry)| { + ( + index, + zcash_history::Entry::from_raw_bytes_padded(&entry.inner), + ) + }) + .collect(), + current_height: ironwood.current_height, + } + } +} + /// A mirror of [`HistoryTreeParts`] using the pre-NU6.3 [`zcash_history::Entry`] width. #[derive(serde::Serialize, serde::Deserialize)] struct LegacyHistoryTreeParts { @@ -210,18 +263,34 @@ impl IntoDisk for BlockInfo { impl FromDisk for BlockInfo { fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { - // We have two different DB formats, one from NU6_1 (Lockbox) one for Ironwood and onwards. + // We have different DB formats for NU6.1, Ironwood, and NuTachyon onward. const NU6_1_VALUE_BALANCE_LEN: usize = 40; const IRONWOOD_VALUE_BALANCE_LEN: usize = 48; + #[cfg(zcash_unstable = "nutachyon")] + const TACHYON_VALUE_BALANCE_LEN: usize = 56; const BLOCK_SIZE_LEN: usize = 4; const NU6_1_BLOCK_INFO_LEN: usize = NU6_1_VALUE_BALANCE_LEN + BLOCK_SIZE_LEN; const IRONWOOD_BLOCK_INFO_LEN: usize = IRONWOOD_VALUE_BALANCE_LEN + BLOCK_SIZE_LEN; + #[cfg(zcash_unstable = "nutachyon")] + const TACHYON_BLOCK_INFO_LEN: usize = TACHYON_VALUE_BALANCE_LEN + BLOCK_SIZE_LEN; let bytes = bytes.as_ref(); // We want to be forward-compatible, so this must work even if the // size of the buffer is larger than expected. match bytes.len() { + #[cfg(zcash_unstable = "nutachyon")] + TACHYON_BLOCK_INFO_LEN.. => { + let value_pools = + ValueBalance::::from_bytes(&bytes[..TACHYON_VALUE_BALANCE_LEN]) + .expect("must work for 56 bytes"); + let size = u32::from_le_bytes( + bytes[TACHYON_VALUE_BALANCE_LEN..TACHYON_VALUE_BALANCE_LEN + BLOCK_SIZE_LEN] + .try_into() + .expect("must be 4 bytes"), + ); + BlockInfo::new(value_pools, size) + } IRONWOOD_BLOCK_INFO_LEN.. => { let value_pools = ValueBalance::::from_bytes(&bytes[..IRONWOOD_VALUE_BALANCE_LEN]) diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/chain/tests.rs b/crates/zakura-state/src/service/finalized_state/disk_format/chain/tests.rs index b7403a40f8..103ba19953 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/chain/tests.rs +++ b/crates/zakura-state/src/service/finalized_state/disk_format/chain/tests.rs @@ -40,9 +40,13 @@ fn valid_history_tree() -> NonEmptyHistoryTree { sapling_root: &sapling_root, orchard_root: &orchard_root, ironwood_root: &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: &Default::default(), sapling_tx: 0, orchard_tx: 0, ironwood_tx: 0, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tx: 0, }, ) .expect("test history tree must be valid") @@ -73,6 +77,26 @@ fn legacy_parts_from(tree: &NonEmptyHistoryTree) -> LegacyHistoryTreeParts { } } +fn ironwood_parts_from(tree: &NonEmptyHistoryTree) -> IronwoodHistoryTreeParts { + IronwoodHistoryTreeParts { + network_kind: tree.network().kind(), + size: tree.size(), + peaks: tree + .peaks() + .iter() + .map(|(index, entry)| { + let serialized = bincode::DefaultOptions::new() + .serialize(entry) + .expect("history tree entry serialization succeeds"); + let mut inner = [0; IRONWOOD_MAX_ENTRY_SIZE]; + inner.copy_from_slice(&serialized[..IRONWOOD_MAX_ENTRY_SIZE]); + (*index, IronwoodEntry { inner }) + }) + .collect(), + current_height: tree.current_height(), + } +} + fn ephemeral_db(network: &Network) -> ZakuraDb { ZakuraDb::new( &Config::ephemeral(), @@ -135,6 +159,30 @@ fn history_tree_parts_reads_legacy_entry_width() { assert!(parts.as_bytes().len() > legacy_bytes.len()); } +/// A history tree written with the NU6.3 entry width must be read in place and zero-padded to the +/// current width. +#[test] +fn history_tree_parts_reads_ironwood_entry_width() { + let tree = valid_history_tree(); + let ironwood = ironwood_parts_from(&tree); + let ironwood_bytes = bincode::DefaultOptions::new() + .serialize(&ironwood) + .expect("Ironwood serialization succeeds"); + + let parts = + HistoryTreeParts::try_from_bytes(&ironwood_bytes).expect("Ironwood snapshot must decode"); + + assert_eq!(parts.network_kind, tree.network().kind()); + assert_eq!(parts.size, tree.size()); + assert_eq!(parts.current_height, tree.current_height()); + assert_eq!(parts.peaks.len(), tree.peaks().len()); + assert_eq!( + parts.as_bytes(), + HistoryTreeParts::from(ironwood).as_bytes() + ); + assert!(parts.as_bytes().len() > ironwood_bytes.len()); +} + /// A legacy-width row can fail current-width decoding with a non-EOF error if the wider entry /// consumes into the next legacy map entry and interprets entry bytes as bincode control bytes. #[test] @@ -154,7 +202,7 @@ fn history_tree_parts_reads_legacy_entry_width_after_non_eof_current_error() { LegacyEntry { inner: { let mut inner = [0xCD; LEGACY_MAX_ENTRY_SIZE]; - inner[72] = 0xFE; + inner[zcash_history::MAX_ENTRY_SIZE - LEGACY_MAX_ENTRY_SIZE - 1] = 0xFE; inner }, }, @@ -216,7 +264,7 @@ fn history_tree_parts_round_trips_current_width() { } #[test] -fn malformed_history_tree_parts_return_both_decode_errors() { +fn malformed_history_tree_parts_return_all_decode_errors() { let error = match HistoryTreeParts::try_from_bytes([0xFF]) { Ok(_) => panic!("malformed bytes must fail"), Err(error) => error, @@ -226,6 +274,7 @@ fn malformed_history_tree_parts_return_both_decode_errors() { error, HistoryTreeDecodeError::InvalidEncoding { current: _, + ironwood: _, legacy: _ } )); @@ -337,6 +386,7 @@ fn try_history_tree_propagates_malformed_snapshot() { error, HistoryTreeDecodeError::InvalidEncoding { current: _, + ironwood: _, legacy: _ } )); diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/header_chain.rs b/crates/zakura-state/src/service/finalized_state/disk_format/header_chain.rs index ebfd66084a..edc2274e78 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/header_chain.rs +++ b/crates/zakura-state/src/service/finalized_state/disk_format/header_chain.rs @@ -528,7 +528,10 @@ mod tests { "missing opened column family {expected}" ); } - assert_eq!(db.format_version_in_code().minor, 1); + assert_eq!( + db.format_version_in_code(), + state_database_format_version_in_code() + ); drop(db); let reopened = ZakuraDb::new( diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/shielded.rs b/crates/zakura-state/src/service/finalized_state/disk_format/shielded.rs index c34ce15806..38dfa64eff 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/shielded.rs +++ b/crates/zakura-state/src/service/finalized_state/disk_format/shielded.rs @@ -7,6 +7,8 @@ use bincode::Options; +#[cfg(zcash_unstable = "nutachyon")] +use zakura_chain::tachyon; use zakura_chain::{ block::{merkle::AuthDataRoot, Height}, ironwood, orchard, sapling, sprout, @@ -87,6 +89,71 @@ impl FromDisk for orchard::tree::Root { } } +#[cfg(zcash_unstable = "nutachyon")] +impl IntoDisk for tachyon::Anchor { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.into() + } +} + +#[cfg(zcash_unstable = "nutachyon")] +impl FromDisk for tachyon::Anchor { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let bytes: [u8; 32] = bytes + .as_ref() + .try_into() + .expect("Tachyon anchors have a fixed 32-byte disk encoding"); + bytes.into() + } +} + +#[cfg(zcash_unstable = "nutachyon")] +impl IntoDisk for tachyon::Tachygram { + type Bytes = [u8; 32]; + + fn as_bytes(&self) -> Self::Bytes { + self.into() + } +} + +#[cfg(zcash_unstable = "nutachyon")] +impl FromDisk for tachyon::Tachygram { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let bytes: [u8; 32] = bytes + .as_ref() + .try_into() + .expect("Tachygrams have a fixed 32-byte disk encoding"); + bytes.into() + } +} + +/// An on-disk Tachyon epoch index, encoded big-endian for sort order. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg(zcash_unstable = "nutachyon")] +pub struct TachyonEpoch(pub u32); + +#[cfg(zcash_unstable = "nutachyon")] +impl IntoDisk for TachyonEpoch { + type Bytes = [u8; 4]; + + fn as_bytes(&self) -> Self::Bytes { + self.0.to_be_bytes() + } +} + +#[cfg(zcash_unstable = "nutachyon")] +impl FromDisk for TachyonEpoch { + fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + let bytes: [u8; 4] = bytes + .as_ref() + .try_into() + .expect("Tachyon epochs have a fixed 4-byte disk encoding"); + Self(u32::from_be_bytes(bytes)) + } +} + /// The per-height Sapling and Orchard note-commitment roots, as stored in the /// `commitment_roots_by_height` index (keyed by [`Height`]). /// diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/prop.rs b/crates/zakura-state/src/service/finalized_state/disk_format/tests/prop.rs index c40206c7a5..ad3c933b94 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/prop.rs +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/prop.rs @@ -576,16 +576,43 @@ fn roundtrip_value_balance() { } #[test] -fn roundtrip_block_info_with_ironwood_value_pool() { +#[cfg(zcash_unstable = "nutachyon")] +fn roundtrip_block_info_with_tachyon_value_pool() { let _init_guard = zakura_test::init(); let ironwood_amount = Amount::::try_from(7).expect("7 zatoshi is a valid amount"); - let block_info = BlockInfo::new(ValueBalance::from_ironwood_amount(ironwood_amount), 123); + let tachyon_amount = Amount::::try_from(13).expect("13 zatoshi is a valid amount"); + let block_info = BlockInfo::new( + (ValueBalance::from_ironwood_amount(ironwood_amount) + + ValueBalance::from_tachyon_amount(tachyon_amount)) + .expect("small non-negative pool balances have a valid sum"), + 123, + ); - assert_eq!(block_info.as_bytes().len(), 52); + assert_eq!(block_info.as_bytes().len(), 60); assert_value_properties(block_info); } +#[test] +fn block_info_decodes_ironwood_value_pools() { + let _init_guard = zakura_test::init(); + + let ironwood_amount = Amount::::try_from(7).expect("7 zatoshi is a valid amount"); + let ironwood_value_pools = ValueBalance::from_ironwood_amount(ironwood_amount); + let mut ironwood_bytes = ironwood_value_pools.as_bytes()[..48].to_vec(); + ironwood_bytes.extend_from_slice(&123_u32.to_le_bytes()); + + let block_info = BlockInfo::from_bytes(ironwood_bytes); + + assert_eq!(block_info.value_pools().ironwood_amount(), ironwood_amount); + #[cfg(zcash_unstable = "nutachyon")] + assert_eq!( + block_info.value_pools().tachyon_amount(), + Amount::::zero() + ); + assert_eq!(block_info.size(), 123); +} + #[test] fn block_info_decodes_legacy_value_pools() { let _init_guard = zakura_test::init(); diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_0.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_0.snap index 8841b8565e..07978821dc 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_0.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_0.snap @@ -5,6 +5,6 @@ expression: cf_data [ KV( k: "000000", - v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + v: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_1.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_1.snap index d67bac0b1e..346db9eff3 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_1.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_1.snap @@ -5,10 +5,10 @@ expression: cf_data [ KV( k: "000000", - v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + v: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", ), KV( k: "000001", - v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051060000", + v: "24f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051060000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_2.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_2.snap index 7adf04d44a..ebda32d2fd 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_2.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@mainnet_2.snap @@ -5,14 +5,14 @@ expression: cf_data [ KV( k: "000000", - v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + v: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", ), KV( k: "000001", - v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051060000", + v: "24f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051060000", ), KV( k: "000002", - v: "6cdc0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051060000", + v: "6cdc02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051060000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_0.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_0.snap index 8841b8565e..07978821dc 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_0.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_0.snap @@ -5,6 +5,6 @@ expression: cf_data [ KV( k: "000000", - v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + v: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_1.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_1.snap index 853b7bec64..0eb5739915 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_1.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_1.snap @@ -5,10 +5,10 @@ expression: cf_data [ KV( k: "000000", - v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + v: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", ), KV( k: "000001", - v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052060000", + v: "24f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052060000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_2.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_2.snap index 29cda9499f..7508da84e0 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_2.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/block_info_raw_data@testnet_2.snap @@ -5,14 +5,14 @@ expression: cf_data [ KV( k: "000000", - v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", + v: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009c060000", ), KV( k: "000001", - v: "24f40000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052060000", + v: "24f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052060000", ), KV( k: "000002", - v: "6cdc0200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052060000", + v: "6cdc02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000052060000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap index 5f9bbd55e3..6a72fd5fcb 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/column_family_names.snap @@ -41,6 +41,10 @@ expression: cf_names "sprout_anchors", "sprout_note_commitment_tree", "sprout_nullifiers", + "tachyon_anchor_by_height", + "tachyon_anchors", + "tachyon_epoch_anchor_by_epoch", + "tachyon_tachygrams", "tip_chain_value_pool", "tx_by_loc", "tx_loc_by_hash", diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap index db60cc5ab7..b7c4315eab 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_0.snap @@ -26,6 +26,10 @@ expression: empty_column_families "sapling_note_commitment_subtree: no entries", "sapling_nullifiers: no entries", "sprout_nullifiers: no entries", + "tachyon_anchor_by_height: no entries", + "tachyon_anchors: no entries", + "tachyon_epoch_anchor_by_epoch: no entries", + "tachyon_tachygrams: no entries", "tx_loc_by_spent_out_loc: no entries", "tx_loc_by_transparent_addr_loc: no entries", "utxo_by_out_loc: no entries", diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap index 6f6e13334c..79765ebce3 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_1.snap @@ -25,6 +25,10 @@ expression: empty_column_families "sapling_note_commitment_subtree: no entries", "sapling_nullifiers: no entries", "sprout_nullifiers: no entries", + "tachyon_anchor_by_height: no entries", + "tachyon_anchors: no entries", + "tachyon_epoch_anchor_by_epoch: no entries", + "tachyon_tachygrams: no entries", "tx_loc_by_spent_out_loc: no entries", "vct_sync_metadata: no entries", "zakura_header_body_size_by_height: no entries", diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap index 6f6e13334c..79765ebce3 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@mainnet_2.snap @@ -25,6 +25,10 @@ expression: empty_column_families "sapling_note_commitment_subtree: no entries", "sapling_nullifiers: no entries", "sprout_nullifiers: no entries", + "tachyon_anchor_by_height: no entries", + "tachyon_anchors: no entries", + "tachyon_epoch_anchor_by_epoch: no entries", + "tachyon_tachygrams: no entries", "tx_loc_by_spent_out_loc: no entries", "vct_sync_metadata: no entries", "zakura_header_body_size_by_height: no entries", diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap index 6d1314558c..3d5c8c9ef3 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@no_blocks.snap @@ -40,6 +40,10 @@ expression: empty_column_families "sprout_anchors: no entries", "sprout_note_commitment_tree: no entries", "sprout_nullifiers: no entries", + "tachyon_anchor_by_height: no entries", + "tachyon_anchors: no entries", + "tachyon_epoch_anchor_by_epoch: no entries", + "tachyon_tachygrams: no entries", "tip_chain_value_pool: no entries", "tx_by_loc: no entries", "tx_loc_by_hash: no entries", diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap index db60cc5ab7..b7c4315eab 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_0.snap @@ -26,6 +26,10 @@ expression: empty_column_families "sapling_note_commitment_subtree: no entries", "sapling_nullifiers: no entries", "sprout_nullifiers: no entries", + "tachyon_anchor_by_height: no entries", + "tachyon_anchors: no entries", + "tachyon_epoch_anchor_by_epoch: no entries", + "tachyon_tachygrams: no entries", "tx_loc_by_spent_out_loc: no entries", "tx_loc_by_transparent_addr_loc: no entries", "utxo_by_out_loc: no entries", diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap index 6f6e13334c..79765ebce3 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_1.snap @@ -25,6 +25,10 @@ expression: empty_column_families "sapling_note_commitment_subtree: no entries", "sapling_nullifiers: no entries", "sprout_nullifiers: no entries", + "tachyon_anchor_by_height: no entries", + "tachyon_anchors: no entries", + "tachyon_epoch_anchor_by_epoch: no entries", + "tachyon_tachygrams: no entries", "tx_loc_by_spent_out_loc: no entries", "vct_sync_metadata: no entries", "zakura_header_body_size_by_height: no entries", diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap index 6f6e13334c..79765ebce3 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/empty_column_families@testnet_2.snap @@ -25,6 +25,10 @@ expression: empty_column_families "sapling_note_commitment_subtree: no entries", "sapling_nullifiers: no entries", "sprout_nullifiers: no entries", + "tachyon_anchor_by_height: no entries", + "tachyon_anchors: no entries", + "tachyon_epoch_anchor_by_epoch: no entries", + "tachyon_tachygrams: no entries", "tx_loc_by_spent_out_loc: no entries", "vct_sync_metadata: no entries", "zakura_header_body_size_by_height: no entries", diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_0.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_0.snap index 265231f491..1387f97a82 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_0.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_0.snap @@ -5,6 +5,6 @@ expression: cf_data [ KV( k: "", - v: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_1.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_1.snap index d0457c51a5..48ed2dbd87 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_1.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_1.snap @@ -5,6 +5,6 @@ expression: cf_data [ KV( k: "", - v: "24f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + v: "24f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_2.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_2.snap index aca4654f3f..c37a66fc80 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_2.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@mainnet_2.snap @@ -5,6 +5,6 @@ expression: cf_data [ KV( k: "", - v: "6cdc02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + v: "6cdc020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_0.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_0.snap index 265231f491..1387f97a82 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_0.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_0.snap @@ -5,6 +5,6 @@ expression: cf_data [ KV( k: "", - v: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + v: "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_1.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_1.snap index d0457c51a5..48ed2dbd87 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_1.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_1.snap @@ -5,6 +5,6 @@ expression: cf_data [ KV( k: "", - v: "24f400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + v: "24f4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_2.snap b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_2.snap index aca4654f3f..c37a66fc80 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_2.snap +++ b/crates/zakura-state/src/service/finalized_state/disk_format/tests/snapshots/tip_chain_value_pool_raw_data@testnet_2.snap @@ -5,6 +5,6 @@ expression: cf_data [ KV( k: "", - v: "6cdc02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + v: "6cdc020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", ), ] diff --git a/crates/zakura-state/src/service/finalized_state/disk_format/upgrade.rs b/crates/zakura-state/src/service/finalized_state/disk_format/upgrade.rs index e0b1f58f27..34b92a560f 100644 --- a/crates/zakura-state/src/service/finalized_state/disk_format/upgrade.rs +++ b/crates/zakura-state/src/service/finalized_state/disk_format/upgrade.rs @@ -111,8 +111,8 @@ fn format_upgrades( let min_version = move || min_version.clone().unwrap_or(Version::new(0, 0, 0)); // Note: Disk format upgrades must be run in order of database version. - ([ - Box::new(block_info_and_address_received::Upgrade), + vec![ + Box::new(block_info_and_address_received::Upgrade) as Box, Box::new(no_migration::NoMigration::new( "add pruning metadata column family", Version::new(27, 1, 0), @@ -144,9 +144,19 @@ fn format_upgrades( )), Box::new(drop_header_root_auth_frontier::Upgrade), Box::new(unauthenticated_commitment_roots::Upgrade), - ] as [Box; 10]) - .into_iter() - .filter(move |upgrade| upgrade.version() > min_version()) + #[cfg(zcash_unstable = "nutachyon")] + Box::new(no_migration::NoMigration::new( + "widen history tree entries for NuTachyon", + Version::new(28, 2, 5), + )), + #[cfg(zcash_unstable = "nutachyon")] + Box::new(no_migration::NoMigration::new( + "add Tachyon state and widen chain value balance and history entries", + Version::new(29, 0, 0), + )), + ] + .into_iter() + .filter(move |upgrade| upgrade.version() > min_version()) } /// Returns a list of all the major db format versions that can restored from the @@ -1109,13 +1119,15 @@ fn vct_format_changes_include_root_auth_metadata_updates() { let upgrades: Vec<_> = format_upgrades(Some(Version::new(27, 3, 0))).collect(); - assert_eq!(upgrades.len(), 6); + assert_eq!(upgrades.len(), 8); assert_eq!(upgrades[0].version(), Version::new(28, 0, 0)); assert_eq!(upgrades[1].version(), Version::new(28, 0, 1)); assert_eq!(upgrades[2].version(), Version::new(28, 0, 2)); assert_eq!(upgrades[3].version(), Version::new(28, 1, 3)); assert_eq!(upgrades[4].version(), Version::new(28, 1, 4)); assert_eq!(upgrades[5].version(), Version::new(28, 1, 5)); + assert_eq!(upgrades[6].version(), Version::new(28, 2, 5)); + assert_eq!(upgrades[7].version(), Version::new(29, 0, 0)); assert!( !upgrades[3].needs_migration(), "the header-chain column families are created on open without rebasing authenticated roots" diff --git a/crates/zakura-state/src/service/finalized_state/tests/prop.rs b/crates/zakura-state/src/service/finalized_state/tests/prop.rs index 672212b220..6bf942e27d 100644 --- a/crates/zakura-state/src/service/finalized_state/tests/prop.rs +++ b/crates/zakura-state/src/service/finalized_state/tests/prop.rs @@ -315,6 +315,8 @@ fn vct_generated_final_frontier_bytes_are_node_loader_compatible() -> Result<()> nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -430,6 +432,8 @@ fn all_upgrades_and_wrong_commitments_with_fake_activation_heights() -> Result<( nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -551,6 +555,8 @@ fn vct_fast_path_matches_legacy_and_rejects_wrong_roots() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -773,6 +779,8 @@ fn vct_frozen_frontier_hole_refuses_instead_of_recomputing() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -878,6 +886,8 @@ fn vct_retryable_root_miss_keeps_checkpoint_response_pending() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -979,6 +989,8 @@ fn vct_untrusted_source_defers_unverifiable_tip_root_until_successor() -> Result nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -1224,6 +1236,8 @@ fn vct_untrusted_source_bad_root_replacement_commits_same_height() -> Result<()> nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -1377,6 +1391,8 @@ fn vct_frozen_frontier_survives_reopen() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -1554,6 +1570,8 @@ fn vct_fast_sync_handoff_marks_database_and_resumes() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -2089,6 +2107,8 @@ fn vct_mode_switches_continue_from_safe_boundaries() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -2294,6 +2314,8 @@ fn vct_dedup_skips_redundant_check_and_guards_stale_cache() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -2566,6 +2588,8 @@ fn vct_clear_prevalidation_cache_disarms_skip_then_dedup_resumes() -> Result<()> nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -2674,6 +2698,8 @@ fn vct_db_produced_payload_round_trips_to_byte_identical_state() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -3071,6 +3097,8 @@ fn vct_untrusted_fixture_drives_byte_identical_state() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() @@ -3245,6 +3273,8 @@ fn vct_read_service_serves_or_refuses_absent_band_treestates() -> Result<()> { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("failed to set activation heights") .extend_funding_streams() diff --git a/crates/zakura-state/src/service/finalized_state/tests/rollback.rs b/crates/zakura-state/src/service/finalized_state/tests/rollback.rs index 61e39ae285..a004ba84e8 100644 --- a/crates/zakura-state/src/service/finalized_state/tests/rollback.rs +++ b/crates/zakura-state/src/service/finalized_state/tests/rollback.rs @@ -870,6 +870,8 @@ fn modern_rollback_network() -> Network { nu6_2: Some(10), nu6_3: Some(11), nu7: Some(12), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("configured activation heights are valid") .extend_funding_streams() diff --git a/crates/zakura-state/src/service/finalized_state/vct_fast_sync_fixture.rs b/crates/zakura-state/src/service/finalized_state/vct_fast_sync_fixture.rs index d038c853c9..bc6eb5a0c0 100644 --- a/crates/zakura-state/src/service/finalized_state/vct_fast_sync_fixture.rs +++ b/crates/zakura-state/src/service/finalized_state/vct_fast_sync_fixture.rs @@ -125,6 +125,8 @@ impl VctFastSyncedChain { nu6_2: Some(47), nu6_3: Some(48), nu7: Some(50), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("the fixture activation heights are ordered") .extend_funding_streams() diff --git a/crates/zakura-state/src/service/finalized_state/zakura_db/rollback.rs b/crates/zakura-state/src/service/finalized_state/zakura_db/rollback.rs index c41b27b94b..355a6b899d 100644 --- a/crates/zakura-state/src/service/finalized_state/zakura_db/rollback.rs +++ b/crates/zakura-state/src/service/finalized_state/zakura_db/rollback.rs @@ -238,6 +238,11 @@ pub enum RollbackFinalizedStateError { #[error("failed to rebuild history tree")] HistoryTree(#[from] HistoryTreeError), + /// Rebuilding the Tachyon anchor failed. + #[error("failed to rebuild the Tachyon anchor")] + #[cfg(zcash_unstable = "nutachyon")] + TachyonAnchor(#[from] zcash_tachyon::AnchorError), + /// Computing a block subsidy failed. #[error("failed to compute block subsidy")] Subsidy(#[from] SubsidyError), @@ -606,14 +611,44 @@ fn rebuild_history_tree_from_upgrade_activation( let (block, sapling_root, orchard_root, ironwood_root) = history_rebuild_inputs_at_height(db, start_height)?; - let mut history_tree = - HistoryTree::from_block(network, block, &sapling_root, &orchard_root, &ironwood_root)?; + #[cfg(zcash_unstable = "nutachyon")] + let mut tachyon_anchor = zakura_chain::tachyon::Anchor::default(); + #[cfg(zcash_unstable = "nutachyon")] + if let Some(pool_height) = zakura_chain::tachyon::pool_height(network, start_height) { + tachyon_anchor = tachyon_anchor + .advance_with_block(pool_height, &block)? + .post_block; + } + let mut history_tree = HistoryTree::from_block( + network, + block, + &sapling_root, + &orchard_root, + &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &tachyon_anchor, + )?; for height in ((start_height.0 + 1)..=target_height.0).map(Height) { let (block, sapling_root, orchard_root, ironwood_root) = history_rebuild_inputs_at_height(db, height)?; - history_tree.push(network, block, &sapling_root, &orchard_root, &ironwood_root)?; + #[cfg(zcash_unstable = "nutachyon")] + if let Some(pool_height) = zakura_chain::tachyon::pool_height(network, height) { + tachyon_anchor = tachyon_anchor + .advance_with_block(pool_height, &block)? + .post_block; + } + + history_tree.push( + network, + block, + &sapling_root, + &orchard_root, + &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &tachyon_anchor, + )?; } Ok(history_tree) @@ -679,12 +714,28 @@ fn rebuild_treestate_to_height( .ok_or(RollbackFinalizedStateError::MissingBlock { height })?; note_commitment_trees.update_trees_parallel(&block)?; + #[cfg(zcash_unstable = "nutachyon")] + if let Some(pool_height) = zakura_chain::tachyon::pool_height(network, height) { + let advance = note_commitment_trees + .tachyon_anchor + .advance_with_block(pool_height, &block)?; + note_commitment_trees.tachyon_anchor = advance.post_block; + note_commitment_trees.tachyon_epoch_anchor = advance.epoch_boundary; + } retained_sprout_roots.insert(note_commitment_trees.sprout.root()); let sapling_root = note_commitment_trees.sapling.root(); let orchard_root = note_commitment_trees.orchard.root(); let ironwood_root = note_commitment_trees.ironwood.root(); - history_tree.push(network, block, &sapling_root, &orchard_root, &ironwood_root)?; + history_tree.push( + network, + block, + &sapling_root, + &orchard_root, + &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + ¬e_commitment_trees.tachyon_anchor, + )?; } Ok(RebuiltTreestate { diff --git a/crates/zakura-state/src/service/finalized_state/zakura_db/shielded.rs b/crates/zakura-state/src/service/finalized_state/zakura_db/shielded.rs index a10688fbd5..841de476ea 100644 --- a/crates/zakura-state/src/service/finalized_state/zakura_db/shielded.rs +++ b/crates/zakura-state/src/service/finalized_state/zakura_db/shielded.rs @@ -17,6 +17,8 @@ use std::{ sync::Arc, }; +#[cfg(zcash_unstable = "nutachyon")] +use zakura_chain::tachyon; use zakura_chain::{ block::Height, ironwood, orchard, @@ -26,6 +28,8 @@ use zakura_chain::{ transaction::Transaction, }; +#[cfg(zcash_unstable = "nutachyon")] +use crate::service::finalized_state::disk_format::shielded::TachyonEpoch; use crate::{ request::{FinalizedBlock, Treestate}, service::finalized_state::{ @@ -182,6 +186,53 @@ impl ZakuraDb { self.db.zs_contains(&ironwood_anchors, &ironwood_anchor) } + /// Returns the finalized height that created `tachyon_anchor` while it remains retained. + #[allow(clippy::unwrap_in_result)] + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_anchor_height(&self, tachyon_anchor: &tachyon::Anchor) -> Option { + let tachyon_anchors = self.db.cf_handle("tachyon_anchors").unwrap(); + self.db.zs_get(&tachyon_anchors, tachyon_anchor) + } + + /// Returns the Tachyon pool anchor after the finalized tip. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_anchor_for_tip(&self) -> tachyon::Anchor { + let anchors_by_height = self.db.cf_handle("tachyon_anchor_by_height").unwrap(); + self.db + .zs_last_key_value(&anchors_by_height) + .map(|(_height, anchor): (Height, tachyon::Anchor)| anchor) + .unwrap_or_default() + } + + /// Returns the finalized height that revealed `tachygram` while it remains retained. + #[allow(clippy::unwrap_in_result)] + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_tachygram_revealed_height( + &self, + tachygram: &tachyon::Tachygram, + ) -> Option { + let tachygrams = self.db.cf_handle("tachyon_tachygrams").unwrap(); + self.db.zs_get(&tachygrams, tachygram) + } + + /// Returns the boundary anchor for a finalized Tachyon epoch. + #[allow(clippy::unwrap_in_result)] + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_epoch_anchor(&self, epoch: u32) -> Option { + let anchors = self.db.cf_handle("tachyon_epoch_anchor_by_epoch").unwrap(); + self.db.zs_get(&anchors, &TachyonEpoch(epoch)) + } + + /// Returns the latest finalized Tachyon epoch. + #[allow(clippy::unwrap_in_result)] + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_current_epoch(&self) -> Option { + let anchors = self.db.cf_handle("tachyon_epoch_anchor_by_epoch").unwrap(); + self.db + .zs_last_key_value(&anchors) + .map(|(epoch, _anchor): (TachyonEpoch, tachyon::Anchor)| epoch.0) + } + // # Sprout trees /// Returns the Sprout note commitment tree of the finalized tip, @@ -716,6 +767,10 @@ impl ZakuraDb { orchard_subtree: self.orchard_subtree_for_tip(), ironwood: self.ironwood_tree_for_tip(), ironwood_subtree: self.ironwood_subtree_for_tip(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: self.tachyon_anchor_for_tip(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_epoch_anchor: None, }) } } @@ -745,6 +800,60 @@ impl DiskWriteBatch { for transaction in &finalized.block.transactions { self.prepare_nullifier_batch(zakura_db, transaction); } + + #[cfg(zcash_unstable = "nutachyon")] + self.prepare_tachyon_tachygram_batch(zakura_db, finalized); + } + + /// Stores this block's Tachygrams and prunes data outside the two-epoch scan window. + #[cfg(zcash_unstable = "nutachyon")] + pub fn prepare_tachyon_tachygram_batch( + &mut self, + zakura_db: &ZakuraDb, + finalized: &FinalizedBlock, + ) { + let height = finalized.height; + let block_tachygrams: Vec<_> = finalized + .block + .transactions + .iter() + .flat_map(|transaction| transaction.tachyon_tachygrams()) + .collect(); + let starts_epoch = + tachyon::pool_height(&zakura_db.network(), height).is_some_and(tachyon::is_epoch_first); + + if !starts_epoch && block_tachygrams.is_empty() { + return; + } + + let tachyon_tachygrams = zakura_db.db.cf_handle("tachyon_tachygrams").unwrap(); + if starts_epoch { + let cutoff = Height(height.0.saturating_sub(tachyon::EPOCH_LENGTH)); + let expired_tachygrams: Vec = zakura_db + .db + .zs_forward_range_iter(&tachyon_tachygrams, ..) + .filter(|&(_, revealed): &(tachyon::Tachygram, Height)| revealed < cutoff) + .map(|(tachygram, _)| tachygram) + .collect(); + for tachygram in expired_tachygrams { + self.zs_delete(&tachyon_tachygrams, tachygram); + } + + let tachyon_anchors = zakura_db.db.cf_handle("tachyon_anchors").unwrap(); + let expired_anchors: Vec = zakura_db + .db + .zs_forward_range_iter(&tachyon_anchors, ..) + .filter(|&(_, anchor_height): &(tachyon::Anchor, Height)| anchor_height < cutoff) + .map(|(anchor, _)| anchor) + .collect(); + for anchor in expired_anchors { + self.zs_delete(&tachyon_anchors, anchor); + } + } + + for tachygram in block_tachygrams { + self.zs_insert(&tachyon_tachygrams, tachygram, height); + } } /// Prepare a database batch containing `finalized.block`'s nullifiers, @@ -825,6 +934,26 @@ impl DiskWriteBatch { let orchard_tx = finalized.block.orchard_transactions_count(); let ironwood_tx = finalized.block.ironwood_transactions_count(); + #[cfg(zcash_unstable = "nutachyon")] + let previous_tachyon_anchor = prev_note_commitment_trees.as_ref().map_or_else( + || zakura_db.tachyon_anchor_for_tip(), + |trees| trees.tachyon_anchor, + ); + #[cfg(zcash_unstable = "nutachyon")] + if previous_tachyon_anchor != note_commitment_trees.tachyon_anchor { + self.create_tachyon_anchor(zakura_db, height, ¬e_commitment_trees.tachyon_anchor); + } + #[cfg(zcash_unstable = "nutachyon")] + if let Some(epoch_anchor) = note_commitment_trees.tachyon_epoch_anchor { + let epoch = tachyon::epoch(&zakura_db.network(), *height) + .expect("a block carrying a Tachyon epoch anchor is NuTachyon-onward"); + let epoch_anchors = zakura_db + .db + .cf_handle("tachyon_epoch_anchor_by_epoch") + .unwrap(); + self.zs_insert(&epoch_anchors, TachyonEpoch(epoch), epoch_anchor); + } + // Record the upgrade height `U` once, on the first block this binary commits: the lowest // height in the serving index, and the boundary below which roots are served from the // pre-upgrade per-height trees instead. Written on both commit paths so it is set even for @@ -1097,6 +1226,20 @@ impl DiskWriteBatch { self.zs_insert(&sapling_subtree_cf, subtree.index, subtree.into_data()); } + /// Stores an end-of-block Tachyon anchor by value and height. + #[cfg(zcash_unstable = "nutachyon")] + pub fn create_tachyon_anchor( + &mut self, + zakura_db: &ZakuraDb, + height: &Height, + anchor: &tachyon::Anchor, + ) { + let anchors = zakura_db.db.cf_handle("tachyon_anchors").unwrap(); + let anchors_by_height = zakura_db.db.cf_handle("tachyon_anchor_by_height").unwrap(); + self.zs_insert(&anchors, anchor, height); + self.zs_insert(&anchors_by_height, height, anchor); + } + /// Deletes the Sapling note commitment tree at the given [`Height`]. pub fn delete_sapling_tree(&mut self, zakura_db: &ZakuraDb, height: &Height) { let sapling_tree_cf = zakura_db diff --git a/crates/zakura-state/src/service/non_finalized_state.rs b/crates/zakura-state/src/service/non_finalized_state.rs index eb58c2e5a9..7a24d31695 100644 --- a/crates/zakura-state/src/service/non_finalized_state.rs +++ b/crates/zakura-state/src/service/non_finalized_state.rs @@ -473,6 +473,8 @@ impl NonFinalizedState { finalized_state.sapling_tree_for_tip(), finalized_state.orchard_tree_for_tip(), finalized_state.ironwood_tree_for_tip(), + #[cfg(zcash_unstable = "nutachyon")] + finalized_state.tachyon_anchor_for_tip(), finalized_state.history_tree(), finalized_state.finalized_value_pool(), ); @@ -538,6 +540,8 @@ impl NonFinalizedState { finalized_state.sapling_tree_for_tip(), finalized_state.orchard_tree_for_tip(), finalized_state.ironwood_tree_for_tip(), + #[cfg(zcash_unstable = "nutachyon")] + finalized_state.tachyon_anchor_for_tip(), finalized_state.history_tree(), finalized_state.finalized_value_pool(), ); diff --git a/crates/zakura-state/src/service/non_finalized_state/chain.rs b/crates/zakura-state/src/service/non_finalized_state/chain.rs index 71c2722b1c..2af19409bf 100644 --- a/crates/zakura-state/src/service/non_finalized_state/chain.rs +++ b/crates/zakura-state/src/service/non_finalized_state/chain.rs @@ -11,6 +11,8 @@ use std::{ use chrono::{DateTime, Utc}; use tracing::instrument; +#[cfg(zcash_unstable = "nutachyon")] +use zakura_chain::tachyon; use zakura_chain::{ amount::{Amount, NegativeAllowed, NonNegative}, block::{self, Height}, @@ -217,6 +219,22 @@ pub struct ChainInner { /// This extra root is removed when the first non-finalized block is committed. pub(crate) ironwood_anchors_by_height: BTreeMap, + /// Tachyon end-of-block anchors and the heights that created them. + #[cfg(zcash_unstable = "nutachyon")] + pub(crate) tachyon_anchors: HashMap>, + /// Tachyon end-of-block anchors indexed by height. + #[cfg(zcash_unstable = "nutachyon")] + pub(crate) tachyon_anchors_by_height: BTreeMap, + /// Tachyon epoch-boundary anchors indexed by epoch. + #[cfg(zcash_unstable = "nutachyon")] + pub(crate) tachyon_epoch_anchors_by_epoch: BTreeMap, + /// Tachygrams and every non-finalized height that revealed them. + #[cfg(zcash_unstable = "nutachyon")] + pub(crate) tachyon_tachygrams: HashMap>, + /// Tachygrams revealed by each non-finalized block. + #[cfg(zcash_unstable = "nutachyon")] + pub(crate) tachyon_tachygrams_by_height: BTreeMap>, + // Nullifiers // /// The Sprout nullifiers revealed by `blocks` and, if the `indexer` feature is selected, @@ -269,6 +287,7 @@ impl Chain { sapling_note_commitment_tree: Arc, orchard_note_commitment_tree: Arc, ironwood_note_commitment_tree: Arc, + #[cfg(zcash_unstable = "nutachyon")] finalized_tip_tachyon_anchor: tachyon::Anchor, history_tree: Arc, finalized_tip_chain_value_pools: ValueBalance, ) -> Self { @@ -294,6 +313,16 @@ impl Chain { ironwood_anchors_by_height: Default::default(), ironwood_trees_by_height: Default::default(), ironwood_subtrees: Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchors: Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchors_by_height: Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_epoch_anchors_by_epoch: Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tachygrams: Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_tachygrams_by_height: Default::default(), sprout_nullifiers: Default::default(), sapling_nullifiers: Default::default(), orchard_nullifiers: Default::default(), @@ -317,6 +346,13 @@ impl Chain { chain.add_ironwood_tree_and_anchor(finalized_tip_height, ironwood_note_commitment_tree); chain.add_history_tree(finalized_tip_height, history_tree); + // Only seed a real Tachyon anchor: the default anchor means the pool has not + // started (pre-NuTachyon finalized tip), and only NuTachyon-onward anchors are tracked. + #[cfg(zcash_unstable = "nutachyon")] + if finalized_tip_tachyon_anchor != tachyon::Anchor::default() { + chain.add_tachyon_anchor(finalized_tip_height, finalized_tip_tachyon_anchor); + } + chain } @@ -1316,6 +1352,179 @@ impl Chain { } } + /// Returns the Tachyon anchor after `hash_or_height`. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_anchor(&self, hash_or_height: HashOrHeight) -> Option { + let height = + hash_or_height.height_or_else(|hash| self.height_by_hash.get(&hash).copied())?; + Some( + self.tachyon_anchors_by_height + .range(..=height) + .next_back() + .map(|(_, anchor)| *anchor) + .unwrap_or_default(), + ) + } + + /// Returns the Tachyon anchor after this chain's tip. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_anchor_for_tip(&self) -> tachyon::Anchor { + self.tachyon_anchors_by_height + .last_key_value() + .map(|(_, anchor)| *anchor) + .unwrap_or_default() + } + + #[cfg(zcash_unstable = "nutachyon")] + fn add_tachyon_anchor(&mut self, height: Height, anchor: tachyon::Anchor) { + assert_eq!( + self.tachyon_anchors_by_height.insert(height, anchor), + None, + "Tachyon anchors must be reverted before replacement", + ); + self.tachyon_anchors.entry(anchor).or_default().push(height); + } + + #[cfg(zcash_unstable = "nutachyon")] + fn remove_tachyon_anchor(&mut self, position: RevertPosition, height: Height) { + let removed: Vec<_> = if position == RevertPosition::Root { + let removed = self + .tachyon_anchors_by_height + .range(..=height) + .map(|(height, anchor)| (*height, *anchor)) + .collect::>(); + for (height, _) in &removed { + self.tachyon_anchors_by_height.remove(height); + } + removed + } else { + self.tachyon_anchors_by_height + .remove(&height) + .map(|anchor| (height, anchor)) + .into_iter() + .collect() + }; + + for (anchor_height, anchor) in removed { + let heights = self + .tachyon_anchors + .get_mut(&anchor) + .expect("the Tachyon anchor value index mirrors its height index"); + let index = heights + .iter() + .position(|&height| height == anchor_height) + .expect("the Tachyon anchor height exists in both indexes"); + heights.swap_remove(index); + if heights.is_empty() { + self.tachyon_anchors.remove(&anchor); + } + } + } + + /// Returns the boundary anchor for `epoch`. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_epoch_anchor(&self, epoch: u32) -> Option { + self.tachyon_epoch_anchors_by_epoch.get(&epoch).copied() + } + + /// Returns the Tachyon epoch containing this chain's tip. + #[cfg(zcash_unstable = "nutachyon")] + pub fn tachyon_current_epoch(&self) -> Option { + let tip_height = (!self.is_empty()).then(|| self.non_finalized_tip_height())?; + tachyon::epoch(&self.network, tip_height) + } + + #[cfg(zcash_unstable = "nutachyon")] + fn add_tachyon_epoch_anchor(&mut self, epoch: u32, anchor: tachyon::Anchor) { + assert_eq!( + self.tachyon_epoch_anchors_by_epoch.insert(epoch, anchor), + None, + "a Tachyon epoch starts at most once per chain", + ); + } + + #[cfg(zcash_unstable = "nutachyon")] + fn remove_tachyon_epoch_anchor(&mut self, position: RevertPosition, height: Height) { + let Some(pool_height) = tachyon::pool_height(&self.network, height) else { + return; + }; + if position == RevertPosition::Root { + let root_epoch = tachyon::epoch_of_pool_height(pool_height); + let epochs = self + .tachyon_epoch_anchors_by_epoch + .range(..=root_epoch) + .map(|(&epoch, _)| epoch) + .collect::>(); + for epoch in epochs { + self.tachyon_epoch_anchors_by_epoch.remove(&epoch); + } + } else if tachyon::is_epoch_first(pool_height) { + self.tachyon_epoch_anchors_by_epoch + .remove(&tachyon::epoch_of_pool_height(pool_height)); + } + } + + #[cfg(zcash_unstable = "nutachyon")] + fn add_tachyon_tachygrams( + &mut self, + height: Height, + tachygrams: Vec, + ) -> Result<(), ValidateContextError> { + if tachygrams.is_empty() { + return Ok(()); + } + + let network = self.network.clone(); + for &tachygram in &tachygrams { + let revealed_heights = self.tachyon_tachygrams.entry(tachygram).or_default(); + if revealed_heights + .iter() + .any(|&revealed| tachyon::within_scan_window(&network, revealed, height)) + { + return Err(ValidateContextError::DuplicateTachyonTachygram { + tachygram, + in_finalized_state: false, + }); + } + revealed_heights.push(height); + } + self.tachyon_tachygrams_by_height.insert(height, tachygrams); + Ok(()) + } + + #[cfg(zcash_unstable = "nutachyon")] + fn remove_tachyon_tachygrams(&mut self, position: RevertPosition, height: Height) { + let removed_heights: Vec<_> = if position == RevertPosition::Root { + self.tachyon_tachygrams_by_height + .range(..=height) + .map(|(height, _)| *height) + .collect() + } else { + self.tachyon_tachygrams_by_height + .contains_key(&height) + .then_some(height) + .into_iter() + .collect() + }; + + for height in removed_heights { + let tachygrams = self + .tachyon_tachygrams_by_height + .remove(&height) + .expect("the Tachygram height was selected from this index"); + for tachygram in tachygrams { + let revealed_heights = self + .tachyon_tachygrams + .get_mut(&tachygram) + .expect("the Tachygram value index mirrors its height index"); + revealed_heights.retain(|&revealed| revealed != height); + if revealed_heights.is_empty() { + self.tachyon_tachygrams.remove(&tachygram); + } + } + } + } + /// Returns the History tree of the tip of this [`Chain`], /// including all finalized blocks, and the non-finalized blocks below the chain tip. /// @@ -1391,6 +1600,17 @@ impl Chain { let sapling_subtree = self.sapling_subtree(hash_or_height); let orchard_subtree = self.orchard_subtree(hash_or_height); let ironwood_subtree = self.ironwood_subtree(hash_or_height); + #[cfg(zcash_unstable = "nutachyon")] + let tachyon_anchor = self.tachyon_anchor(hash_or_height)?; + #[cfg(zcash_unstable = "nutachyon")] + let height = + hash_or_height.height_or_else(|hash| self.height_by_hash.get(&hash).copied())?; + #[cfg(zcash_unstable = "nutachyon")] + let tachyon_epoch_anchor = tachyon::pool_height(&self.network, height) + .filter(|&pool_height| tachyon::is_epoch_first(pool_height)) + .and_then(|pool_height| { + self.tachyon_epoch_anchor(tachyon::epoch_of_pool_height(pool_height)) + }); Some(Treestate::new( sprout_tree, @@ -1400,6 +1620,10 @@ impl Chain { sapling_subtree, orchard_subtree, ironwood_subtree, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor, + #[cfg(zcash_unstable = "nutachyon")] + tachyon_epoch_anchor, history_tree, )) } @@ -1687,6 +1911,10 @@ impl Chain { orchard_subtree: self.orchard_subtree_for_tip(), ironwood: self.ironwood_note_commitment_tree_for_tip(), ironwood_subtree: self.ironwood_subtree_for_tip(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_anchor: self.tachyon_anchor_for_tip(), + #[cfg(zcash_unstable = "nutachyon")] + tachyon_epoch_anchor: None, }; let mut tree_result = None; @@ -1731,6 +1959,40 @@ impl Chain { let orchard_root = self.orchard_note_commitment_tree_for_tip().root(); let ironwood_root = self.ironwood_note_commitment_tree_for_tip().root(); + // Advance the Tachyon pool anchor with this block, if the pool has started. + // `nct.tachyon_anchor` still holds the parent block's anchor here. + // + // In builds without tachyon support V7 transactions cannot be parsed, so a NuTachyon + // chain cannot be synced and the anchor legitimately stays at its default. + #[cfg(zcash_unstable = "nutachyon")] + { + if let Some(pool_height) = zakura_chain::tachyon::pool_height(&self.network, height) { + let advance = nct + .tachyon_anchor + .advance_with_block(pool_height, &contextually_valid.block)?; + nct.tachyon_anchor = advance.post_block; + nct.tachyon_epoch_anchor = advance.epoch_boundary; + self.add_tachyon_anchor(height, advance.post_block); + if let Some(boundary_anchor) = advance.epoch_boundary { + self.add_tachyon_epoch_anchor( + zakura_chain::tachyon::epoch_of_pool_height(pool_height), + boundary_anchor, + ); + } + } + // Add this block's tachygrams to the epoch working set, rejecting same-epoch + // duplicates within this chain. + let block_tachygrams: Vec<_> = contextually_valid + .block + .transactions + .iter() + .flat_map(|transaction| transaction.tachyon_tachygrams()) + .collect(); + self.add_tachyon_tachygrams(height, block_tachygrams)?; + } + #[cfg(zcash_unstable = "nutachyon")] + let tachyon_anchor = nct.tachyon_anchor; + // TODO: update the history trees in a rayon thread, if they show up in CPU profiles let mut history_tree = self.history_block_commitment_tree(); let history_tree_mut = Arc::make_mut(&mut history_tree); @@ -1741,6 +2003,8 @@ impl Chain { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &tachyon_anchor, ) .map_err(Arc::new)?; @@ -1863,6 +2127,23 @@ impl Chain { orchard_shielded_data, ironwood_shielded_data, ), + #[cfg(zcash_unstable = "nutachyon")] + V7 { + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + ironwood_shielded_data, + .. + } => ( + inputs, + outputs, + &None, + &None, + sapling_shielded_data, + orchard_shielded_data, + ironwood_shielded_data, + ), V1 { .. } | V2 { .. } | V3 { .. } => unreachable!( "older transaction versions only exist in finalized blocks, because of the mandatory canopy checkpoint", @@ -2072,6 +2353,23 @@ impl UpdateWith for Chain { orchard_shielded_data, ironwood_shielded_data, ), + #[cfg(zcash_unstable = "nutachyon")] + V7 { + inputs, + outputs, + sapling_shielded_data, + orchard_shielded_data, + ironwood_shielded_data, + .. + } => ( + inputs, + outputs, + &None, + &None, + sapling_shielded_data, + orchard_shielded_data, + ironwood_shielded_data, + ), V1 { .. } | V2 { .. } | V3 { .. } => unreachable!( "older transaction versions only exist in finalized blocks, because of the mandatory canopy checkpoint", @@ -2116,6 +2414,12 @@ impl UpdateWith for Chain { self.remove_sapling_tree_and_anchor(position, height); self.remove_orchard_tree_and_anchor(position, height); self.remove_ironwood_tree_and_anchor(position, height); + #[cfg(zcash_unstable = "nutachyon")] + { + self.remove_tachyon_anchor(position, height); + self.remove_tachyon_epoch_anchor(position, height); + self.remove_tachyon_tachygrams(position, height); + } // TODO: move this to the history tree UpdateWith.revert...()? self.remove_history_tree(position, height); @@ -2726,3 +3030,153 @@ impl Chain { .insert(subtree.index, subtree.into_data()); } } + +#[cfg(all(test, zcash_unstable = "nutachyon"))] +mod tachyon_tests { + use zakura_chain::parameters::testnet::ConfiguredActivationHeights; + + use super::*; + + /// A chain on a regtest network with NuTachyon (tachyon) activated at height 10, forked + /// from a finalized tip at height 9 (so pool height 0 is chain height 10). + fn nutachyon_chain() -> Chain { + let network = Network::new_regtest( + ConfiguredActivationHeights { + canopy: Some(1), + nu5: Some(2), + nu6: Some(3), + nu6_1: Some(4), + nu6_2: Some(5), + nu6_3: Some(6), + nu7: Some(8), + nu_tachyon: Some(10), + ..Default::default() + } + .into(), + ); + + Chain::new( + &network, + Height(9), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ValueBalance::zero(), + ) + } + + fn tachygram(byte: u8) -> tachyon::Tachygram { + tachyon::Tachygram([byte; 32]) + } + + /// The height of the first block of `epoch` on the [`nutachyon_chain`] network + /// (NuTachyon activates at chain height 10). + fn epoch_first_height(epoch: u32) -> Height { + Height(10 + epoch * tachyon::EPOCH_LENGTH) + } + + #[test] + fn same_epoch_duplicate_tachygram_is_rejected() { + let mut chain = nutachyon_chain(); + + chain + .add_tachyon_tachygrams(Height(20), vec![tachygram(1), tachygram(2)]) + .expect("distinct tachygrams are accepted"); + + let result = chain.add_tachyon_tachygrams(Height(30), vec![tachygram(2)]); + assert!( + matches!( + result, + Err(ValidateContextError::DuplicateTachyonTachygram { + in_finalized_state: false, + .. + }) + ), + "expected DuplicateTachyonTachygram, got: {result:?}", + ); + } + + #[test] + fn duplicate_tachygram_scan_window_spans_two_epochs() { + let mut chain = nutachyon_chain(); + + // Epoch 0 (chain heights 10..10+EPOCH_LENGTH). + chain + .add_tachyon_tachygrams(Height(20), vec![tachygram(1)]) + .expect("first reveal is accepted"); + + // The same tachygram in the immediately following epoch is still within the + // two-epoch scan window: a repeat there is exactly what a double spend's + // next-epoch nullifier would look like. + let result = chain.add_tachyon_tachygrams(epoch_first_height(1), vec![tachygram(1)]); + assert!( + result.is_err(), + "adjacent-epoch duplicate must be rejected: {result:?}", + ); + + // Two epochs later the tachygram has left the scan window. (An honest repeat + // out here would imply a nullifier-derivation collision, but consensus no + // longer tracks it.) + chain + .add_tachyon_tachygrams(epoch_first_height(2), vec![tachygram(1)]) + .expect("a tachygram outside the two-epoch window is accepted again"); + + // And the fresh reveal re-arms the window. + let result = + chain.add_tachyon_tachygrams(Height(epoch_first_height(2).0 + 1), vec![tachygram(1)]); + assert!(result.is_err(), "same-epoch duplicate must be rejected"); + } + + #[test] + fn reverting_a_block_restores_its_tachygrams() { + let mut chain = nutachyon_chain(); + + chain + .add_tachyon_tachygrams(Height(20), vec![tachygram(1)]) + .expect("first reveal is accepted"); + chain + .add_tachyon_tachygrams(Height(30), vec![tachygram(2)]) + .expect("distinct tachygram is accepted"); + + // Fork off the tip block: its tachygram becomes revealable again. + chain.remove_tachyon_tachygrams(RevertPosition::Tip, Height(30)); + chain + .add_tachyon_tachygrams(Height(30), vec![tachygram(2)]) + .expect("a reverted tachygram is revealable again"); + + // A root revert (finalization) drops everything at or below the height. + chain.remove_tachyon_tachygrams(RevertPosition::Root, Height(30)); + assert!(chain.tachyon_tachygrams.is_empty()); + assert!(chain.tachyon_tachygrams_by_height.is_empty()); + } + + #[test] + fn epoch_anchors_are_tracked_and_reverted() { + let mut chain = nutachyon_chain(); + let anchor_0 = tachyon::Anchor([10; 32]); + let anchor_1 = tachyon::Anchor([11; 32]); + + chain.add_tachyon_epoch_anchor(0, anchor_0); + chain.add_tachyon_epoch_anchor(1, anchor_1); + assert_eq!(chain.tachyon_epoch_anchor(0), Some(anchor_0)); + assert_eq!(chain.tachyon_epoch_anchor(1), Some(anchor_1)); + + // Reverting epoch 1's first block from the tip removes its epoch anchor. + chain.remove_tachyon_epoch_anchor(RevertPosition::Tip, epoch_first_height(1)); + assert_eq!(chain.tachyon_epoch_anchor(1), None); + assert_eq!(chain.tachyon_epoch_anchor(0), Some(anchor_0)); + + // Reverting a mid-epoch block from the tip removes nothing. + chain.remove_tachyon_epoch_anchor(RevertPosition::Tip, Height(20)); + assert_eq!(chain.tachyon_epoch_anchor(0), Some(anchor_0)); + + // A root revert at an epoch's first block drops that epoch and everything before it. + chain.add_tachyon_epoch_anchor(1, anchor_1); + chain.remove_tachyon_epoch_anchor(RevertPosition::Root, epoch_first_height(0)); + assert_eq!(chain.tachyon_epoch_anchor(0), None); + assert_eq!(chain.tachyon_epoch_anchor(1), Some(anchor_1)); + } +} diff --git a/crates/zakura-state/src/service/non_finalized_state/tests/prop.rs b/crates/zakura-state/src/service/non_finalized_state/tests/prop.rs index 28a9e06807..8aaa60254d 100644 --- a/crates/zakura-state/src/service/non_finalized_state/tests/prop.rs +++ b/crates/zakura-state/src/service/non_finalized_state/tests/prop.rs @@ -71,7 +71,7 @@ fn push_genesis_chain() -> Result<()> { |((chain, count, network, empty_tree) in PreparedChain::default())| { prop_assert!(empty_tree.is_none()); - let mut only_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), empty_tree, ValueBalance::zero()); + let mut only_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), #[cfg(zcash_unstable = "nutachyon")] Default::default(), empty_tree, ValueBalance::zero()); // contains the block value pool changes and chain value pool balances for each height let mut chain_values = BTreeMap::new(); @@ -124,7 +124,7 @@ fn push_history_tree_chain() -> Result<()> { let count = std::cmp::min(count, chain.len() - 1); let chain = &chain[1..]; - let mut only_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), finalized_tree, ValueBalance::zero()); + let mut only_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), #[cfg(zcash_unstable = "nutachyon")] Default::default(), finalized_tree, ValueBalance::zero()); for block in chain .iter() @@ -173,6 +173,8 @@ fn forked_equals_pushed_genesis() -> Result<()> { Default::default(), Default::default(), Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), empty_tree.clone(), ValueBalance::zero(), ); @@ -195,6 +197,8 @@ fn forked_equals_pushed_genesis() -> Result<()> { Default::default(), Default::default(), Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), empty_tree, ValueBalance::zero(), ); @@ -293,8 +297,8 @@ fn forked_equals_pushed_history_tree() -> Result<()> { // use `fork_at_count` as the fork tip let fork_tip_hash = chain[fork_at_count - 1].hash; - let mut full_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), finalized_tree.clone(), ValueBalance::zero()); - let mut partial_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), finalized_tree, ValueBalance::zero()); + let mut full_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), #[cfg(zcash_unstable = "nutachyon")] Default::default(), finalized_tree.clone(), ValueBalance::zero()); + let mut partial_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), #[cfg(zcash_unstable = "nutachyon")] Default::default(), finalized_tree, ValueBalance::zero()); for block in chain .iter() @@ -361,7 +365,7 @@ fn finalized_equals_pushed_genesis() -> Result<()> { let fake_value_pool = ValueBalance::::fake_populated_pool(); - let mut full_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), empty_tree, fake_value_pool); + let mut full_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), #[cfg(zcash_unstable = "nutachyon")] Default::default(), empty_tree, fake_value_pool); for block in chain .clone() .take(finalized_count) { @@ -375,6 +379,8 @@ fn finalized_equals_pushed_genesis() -> Result<()> { full_chain.sapling_note_commitment_tree_for_tip(), full_chain.orchard_note_commitment_tree_for_tip(), full_chain.ironwood_note_commitment_tree_for_tip(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), full_chain.history_block_commitment_tree(), full_chain.chain_value_pools, ); @@ -439,7 +445,7 @@ fn finalized_equals_pushed_history_tree() -> Result<()> { let fake_value_pool = ValueBalance::::fake_populated_pool(); - let mut full_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), finalized_tree, fake_value_pool); + let mut full_chain = Chain::new(&network, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), #[cfg(zcash_unstable = "nutachyon")] Default::default(), finalized_tree, fake_value_pool); for block in chain .iter() .take(finalized_count) @@ -454,6 +460,8 @@ fn finalized_equals_pushed_history_tree() -> Result<()> { full_chain.sapling_note_commitment_tree_for_tip(), full_chain.orchard_note_commitment_tree_for_tip(), full_chain.ironwood_note_commitment_tree_for_tip(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), full_chain.history_block_commitment_tree(), full_chain.chain_value_pools, ); @@ -629,6 +637,8 @@ fn different_blocks_different_chains() -> Result<()> { &Default::default(), &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ).unwrap().into() ) } else { @@ -642,14 +652,16 @@ fn different_blocks_different_chains() -> Result<()> { &Default::default(), &Default::default(), &Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ).unwrap().into() ) } else { Default::default() }; - let chain1 = Chain::new(&Network::Mainnet, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), finalized_tree1, ValueBalance::fake_populated_pool()); - let chain2 = Chain::new(&Network::Mainnet, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), finalized_tree2, ValueBalance::fake_populated_pool()); + let chain1 = Chain::new(&Network::Mainnet, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), #[cfg(zcash_unstable = "nutachyon")] Default::default(), finalized_tree1, ValueBalance::fake_populated_pool()); + let chain2 = Chain::new(&Network::Mainnet, Height(0), Default::default(), Default::default(), Default::default(), Default::default(), #[cfg(zcash_unstable = "nutachyon")] Default::default(), finalized_tree2, ValueBalance::fake_populated_pool()); let block1 = vec1[1].clone().prepare().test_with_zero_spent_utxos(); let block2 = vec2[1].clone().prepare().test_with_zero_spent_utxos(); diff --git a/crates/zakura-state/src/service/non_finalized_state/tests/vectors.rs b/crates/zakura-state/src/service/non_finalized_state/tests/vectors.rs index 6395ea9550..fcbd2d00bc 100644 --- a/crates/zakura-state/src/service/non_finalized_state/tests/vectors.rs +++ b/crates/zakura-state/src/service/non_finalized_state/tests/vectors.rs @@ -35,6 +35,8 @@ fn construct_empty() { Default::default(), Default::default(), Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), Default::default(), ValueBalance::zero(), ); @@ -53,6 +55,8 @@ fn construct_single() -> Result<()> { Default::default(), Default::default(), Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), Default::default(), ValueBalance::fake_populated_pool(), ); @@ -88,6 +92,8 @@ fn construct_many() -> Result<()> { Default::default(), Default::default(), Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), Default::default(), ValueBalance::fake_populated_pool(), ); @@ -116,6 +122,8 @@ fn ord_matches_work() -> Result<()> { Default::default(), Default::default(), Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), Default::default(), ValueBalance::fake_populated_pool(), ); @@ -128,6 +136,8 @@ fn ord_matches_work() -> Result<()> { Default::default(), Default::default(), Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), Default::default(), ValueBalance::zero(), ); @@ -239,6 +249,8 @@ fn finalize_drops_empty_side_chain_for_network(network: Network) -> Result<()> { Default::default(), Default::default(), Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), Default::default(), ValueBalance::fake_populated_pool(), ) @@ -986,6 +998,8 @@ fn history_tree_is_updated_for_network_upgrade( &chain.sapling_note_commitment_tree_for_tip().root(), &chain.orchard_note_commitment_tree_for_tip().root(), &chain.ironwood_note_commitment_tree_for_tip().root(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .unwrap(); @@ -1066,6 +1080,8 @@ fn commitment_is_validated_for_network_upgrade(network: Network, network_upgrade &chain.sapling_note_commitment_tree_for_tip().root(), &chain.orchard_note_commitment_tree_for_tip().root(), &chain.ironwood_note_commitment_tree_for_tip().root(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .unwrap(); @@ -1172,6 +1188,8 @@ fn fork_drops_subtrees_above_fork_point() -> Result<()> { Default::default(), Default::default(), Default::default(), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), Default::default(), ValueBalance::fake_populated_pool(), ); diff --git a/crates/zakura-state/src/service/write/tests/full_state_coherence.rs b/crates/zakura-state/src/service/write/tests/full_state_coherence.rs index f6775a0759..3f0f69948c 100644 --- a/crates/zakura-state/src/service/write/tests/full_state_coherence.rs +++ b/crates/zakura-state/src/service/write/tests/full_state_coherence.rs @@ -176,6 +176,8 @@ fn generated_nu5_graph_matches_full_state_before_finalization() { nu6_2: Some(120), nu6_3: Some(130), nu7: Some(140), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("the compressed activation schedule is ordered") .with_disable_pow(true) @@ -315,6 +317,8 @@ fn generated_nu5_graph_matches_full_state_before_finalization() { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("the deterministic history tree advances"); blocks.push(block); diff --git a/crates/zakura-state/src/service/write/vct_authentication_sweep/tests.rs b/crates/zakura-state/src/service/write/vct_authentication_sweep/tests.rs index cc02a59c38..8863eb768d 100644 --- a/crates/zakura-state/src/service/write/vct_authentication_sweep/tests.rs +++ b/crates/zakura-state/src/service/write/vct_authentication_sweep/tests.rs @@ -108,6 +108,8 @@ fn parameters(checkpoint_blocks: Option<&[Arc]>) -> Network { nu6_2: Some(NU6_2), nu6_3: Some(NU6_3), nu7: Some(NU7), + #[cfg(zcash_unstable = "nutachyon")] + nu_tachyon: None, }) .expect("the compressed activation schedule is ordered") .with_disable_pow(true) @@ -201,6 +203,16 @@ fn generate_chain(network: &Network) -> Vec> { sapling_shielded_data: None, orchard_shielded_data: None, }, + #[cfg(zcash_unstable = "nutachyon")] + NetworkUpgrade::NuTachyon => Transaction::V5 { + network_upgrade: upgrade, + lock_time: LockTime::unlocked(), + expiry_height: height, + inputs: vec![input], + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + }, }; let transactions = vec![Arc::new(transaction)]; let merkle_root = transactions.iter().cloned().collect(); @@ -251,6 +263,8 @@ fn generate_chain(network: &Network) -> Vec> { &sapling_root, &orchard_root, &ironwood_root, + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("the deterministic history tree advances"); blocks.push(block); @@ -641,6 +655,8 @@ impl Fixture { &empty_sapling_root(), &empty_orchard_root(), &empty_ironwood_root(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("the generated history tree advances"); } @@ -666,6 +682,8 @@ impl Fixture { verify_commitment_roots( &self.network, self.history_tree_through(parent), + #[cfg(zcash_unstable = "nutachyon")] + Default::default(), vec![ CommitmentRootVerification::with_roots( block.clone(), @@ -1084,6 +1102,8 @@ fn fold_placement_is_checked_before_every_history_tree_push() { &empty_sapling_root(), &empty_orchard_root(), &empty_ironwood_root(), + #[cfg(zcash_unstable = "nutachyon")] + &Default::default(), ) .expect("the activation block creates the tree"); diff --git a/crates/zakura-state/src/tests.rs b/crates/zakura-state/src/tests.rs index 4be00ad1f2..2ecbfe0eeb 100644 --- a/crates/zakura-state/src/tests.rs +++ b/crates/zakura-state/src/tests.rs @@ -37,6 +37,8 @@ impl FakeChainHelper for Arc { Transaction::V4 { inputs, .. } => &mut inputs[0], Transaction::V5 { inputs, .. } => &mut inputs[0], Transaction::V6 { inputs, .. } => &mut inputs[0], + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { inputs, .. } => &mut inputs[0], }; match input { diff --git a/crates/zakura-utils/coverage b/crates/zakura-utils/coverage index 942681d4b2..d3509ca8b5 100755 --- a/crates/zakura-utils/coverage +++ b/crates/zakura-utils/coverage @@ -5,7 +5,7 @@ set -o xtrace export CARGO_TARGET_DIR="coverage-target" export SKIP_NETWORK_TESTS=1 export LLVM_PROFILE_FILE="${PWD}/$CARGO_TARGET_DIR/coverage/test.%m.profraw" -export RUSTFLAGS="-Zinstrument-coverage -C link-dead-code -C debuginfo=2" +export RUSTFLAGS='--cfg zcash_unstable="nutachyon" -Zinstrument-coverage -C link-dead-code -C debuginfo=2' rm -rf ./"$CARGO_TARGET_DIR"/coverage mkdir -p ./$CARGO_TARGET_DIR/coverage diff --git a/crates/zakurad/src/components/mempool/storage/tests/prop.rs b/crates/zakurad/src/components/mempool/storage/tests/prop.rs index d1423b1762..ac03f3fcdb 100644 --- a/crates/zakurad/src/components/mempool/storage/tests/prop.rs +++ b/crates/zakurad/src/components/mempool/storage/tests/prop.rs @@ -661,6 +661,8 @@ impl SpendConflictTestInput { // No JoinSplits Transaction::V1 { .. } | Transaction::V5 { .. } => {} Transaction::V6 { .. } => {} + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { .. } => {} } } } @@ -737,6 +739,13 @@ impl SpendConflictTestInput { } => { Self::remove_sapling_transfers_with_conflicts(sapling_shielded_data, &conflicts) } + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + sapling_shielded_data, + .. + } => { + Self::remove_sapling_transfers_with_conflicts(sapling_shielded_data, &conflicts) + } // No Spends Transaction::V1 { .. } | Transaction::V2 { .. } | Transaction::V3 { .. } => {} @@ -813,6 +822,11 @@ impl SpendConflictTestInput { orchard_shielded_data, .. } => Self::remove_orchard_actions_with_conflicts(orchard_shielded_data, &conflicts), + #[cfg(zcash_unstable = "nutachyon")] + Transaction::V7 { + orchard_shielded_data, + .. + } => Self::remove_orchard_actions_with_conflicts(orchard_shielded_data, &conflicts), // No Spends Transaction::V1 { .. } diff --git a/docs/changelog/unreleased/795.md b/docs/changelog/unreleased/795.md new file mode 100644 index 0000000000..cc2a8827ad --- /dev/null +++ b/docs/changelog/unreleased/795.md @@ -0,0 +1,5 @@ +## Added + +- Added NuTachyon network-upgrade and V7 transaction support, including Tachyon bundle + serialization, consensus validation, chain state, and block-template aggregation + ([#795](https://github.com/zakura-core/zakura/pull/795)).