diff --git a/Cargo.lock b/Cargo.lock index 37a081eda7c..ab57f506772 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1422,7 +1422,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -1550,8 +1550,7 @@ dependencies = [ [[package]] name = "equihash" version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4f333d4ccc9d23c06593733673026efa71a332e028b00f12cf427b9677dce9" +source = "git+https://github.com/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "blake2b_simd", "cc", @@ -1582,7 +1581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -1598,8 +1597,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/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "blake2b_simd", ] @@ -1941,9 +1939,9 @@ dependencies = [ [[package]] name = "halo2_gadgets" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73a5e510d58a07d8ed238a5a8a436fe6c2c79e1bb2611f62688bc65007b4e6e7" +checksum = "45824ce0dd12e91ec0c68ebae2a7ed8ae19b70946624c849add59f1d1a62a143" dependencies = [ "arrayvec", "bitvec", @@ -1979,9 +1977,9 @@ dependencies = [ [[package]] name = "halo2_proofs" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "019561b5f3be60731e7b72f3f7878c5badb4174362d860b03d3cf64cb47f90db" +checksum = "05713f117155643ce10975e0bee44a274bcda2f4bb5ef29a999ad67c1fa8d4d3" dependencies = [ "blake2b_simd", "ff", @@ -2775,7 +2773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.3", + "windows-targets 0.52.6", ] [[package]] @@ -3015,6 +3013,19 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "mock_ragu" +version = "0.0.0" +source = "git+https://github.com/S1nus/tachyon.git?rev=f79ae92e4c4a775929a06add99fcf3925f5fef82#f79ae92e4c4a775929a06add99fcf3925f5fef82" +dependencies = [ + "blake2b_simd", + "ff", + "lazy_static", + "pasta_curves", + "rand_core 0.6.4", + "serde", +] + [[package]] name = "mset" version = "0.1.1" @@ -3183,9 +3194,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1ef66fcf99348242a20d582d7434da381a867df8dc155b3a980eca767c56137" +checksum = "6c01cd4ea711aab5f263f2b7aa6966687a2d6c7df4f78eb1b97a66a7a4e78e3b" dependencies = [ "aes", "bitvec", @@ -3205,6 +3216,7 @@ dependencies = [ "nonempty", "pasta_curves", "rand 0.8.5", + "rand_core 0.6.4", "reddsa", "serde", "sinsemilla", @@ -3870,7 +3882,7 @@ dependencies = [ "once_cell", "socket2 0.6.0", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -4325,7 +4337,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -4408,9 +4420,9 @@ dependencies = [ [[package]] name = "sapling-crypto" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d3c081c83f1dc87403d9d71a06f52301c0aa9ea4c17da2a3435bbf493ffba4" +checksum = "5433ab8c1cd52dfad3b903143e371d5bdd514ab7952f71414e6d66a5da8223cd" dependencies = [ "aes", "bellman", @@ -5031,7 +5043,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -6224,7 +6236,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -6504,9 +6516,9 @@ dependencies = [ [[package]] name = "xdg" -version = "2.5.2" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" [[package]] name = "yaml-rust2" @@ -6545,9 +6557,8 @@ dependencies = [ [[package]] name = "zcash_address" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16da37aff6d1a72a917e649cd337ab750c87efc6eeb36d75a47a9a2f71df195f" +version = "0.10.1" +source = "git+https://github.com/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "bech32", "bs58", @@ -6560,18 +6571,17 @@ dependencies = [ [[package]] name = "zcash_encoding" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca38087e6524e5f51a5b0fb3fc18f36d7b84bf67b2056f494ca0c281590953d" +source = "git+https://github.com/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "core2", + "hex", "nonempty", ] [[package]] name = "zcash_history" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fde17bf53792f9c756b313730da14880257d7661b5bfc69d0571c3a7c11a76d" +source = "git+https://github.com/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "blake2b_simd", "byteorder", @@ -6581,8 +6591,7 @@ dependencies = [ [[package]] name = "zcash_keys" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c115531caa1b7ca5ccd82dc26dbe3ba44b7542e928a3f77cd04abbe3cde4a4f2" +source = "git+https://github.com/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "bech32", "blake2b_simd", @@ -6593,7 +6602,9 @@ dependencies = [ "group", "memuse", "nonempty", + "orchard", "rand_core 0.6.4", + "sapling-crypto", "secrecy", "subtle", "tracing", @@ -6619,9 +6630,8 @@ dependencies = [ [[package]] name = "zcash_primitives" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d9e2a8ea37c3b839dc9079747a25789f50cd6a374086e23fc90951e2bcfa37" +version = "0.26.4" +source = "git+https://github.com/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "bip32", "blake2b_simd", @@ -6641,6 +6651,7 @@ dependencies = [ "memuse", "nonempty", "orchard", + "pasta_curves", "rand 0.8.5", "rand_core 0.6.4", "redjubjub", @@ -6656,15 +6667,15 @@ dependencies = [ "zcash_protocol", "zcash_script", "zcash_spec", + "zcash_tachyon 0.0.0 (git+https://github.com/tachyon-zcash/tachyon.git?rev=a441c0cba73c9accebac324abdbd28b2f9701dd5)", "zcash_transparent", "zip32", ] [[package]] name = "zcash_proofs" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3e1bc0e9006a7fd2e6bb4603b1ad512f87c2e092851a9e1f5282df7dbc87d0" +version = "0.26.1" +source = "git+https://github.com/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "bellman", "blake2b_simd", @@ -6674,7 +6685,6 @@ dependencies = [ "home", "jubjub", "known-folders", - "lazy_static", "rand_core 0.6.4", "redjubjub", "sapling-crypto", @@ -6686,14 +6696,14 @@ dependencies = [ [[package]] name = "zcash_protocol" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01fb174be84135a00fd3799161835e46f320d0028dd9446fdbd97a8d928456b6" +version = "0.7.2" +source = "git+https://github.com/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "core2", "document-features", "hex", "memuse", + "zcash_encoding", ] [[package]] @@ -6702,8 +6712,10 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bed6cf5b2b4361105d4ea06b2752f0c8af4641756c7fbc9858a80af186c234f" dependencies = [ + "bip32", "bitflags 2.9.4", "bounded-vec", + "hex", "ripemd 0.1.3", "secp256k1", "sha1", @@ -6720,11 +6732,41 @@ dependencies = [ "blake2b_simd", ] +[[package]] +name = "zcash_tachyon" +version = "0.0.0" +source = "git+https://github.com/tachyon-zcash/tachyon.git?rev=a441c0cba73c9accebac324abdbd28b2f9701dd5#a441c0cba73c9accebac324abdbd28b2f9701dd5" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "lazy_static", + "pasta_curves", + "rand_core 0.6.4", + "reddsa", +] + +[[package]] +name = "zcash_tachyon" +version = "0.0.0" +source = "git+https://github.com/S1nus/tachyon.git?rev=f79ae92e4c4a775929a06add99fcf3925f5fef82#f79ae92e4c4a775929a06add99fcf3925f5fef82" +dependencies = [ + "blake2b_simd", + "ff", + "group", + "halo2_poseidon", + "lazy_static", + "mock_ragu", + "pasta_curves", + "rand_core 0.6.4", + "reddsa", + "serde", +] + [[package]] name = "zcash_transparent" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539b66f06f6bbdc135b9dc0dba879b0b354b4078d498cd1c4dae28038ec90a33" +version = "0.6.3" +source = "git+https://github.com/tachyon-zcash/librustzcash.git?branch=main#0909972999e21f6ac179d24926f323660bc6aeb6" dependencies = [ "bip32", "blake2b_simd", @@ -6733,6 +6775,7 @@ dependencies = [ "document-features", "getset", "hex", + "nonempty", "ripemd 0.1.3", "secp256k1", "sha2 0.10.9", @@ -6764,6 +6807,7 @@ dependencies = [ "dirs", "ed25519-zebra", "equihash", + "ff", "futures", "group", "halo2_proofs", @@ -6775,6 +6819,7 @@ dependencies = [ "lazy_static", "num-integer", "orchard", + "pasta_curves", "primitive-types", "proptest", "proptest-derive", @@ -6808,6 +6853,7 @@ dependencies = [ "zcash_primitives", "zcash_protocol", "zcash_script", + "zcash_tachyon 0.0.0 (git+https://github.com/S1nus/tachyon.git?rev=f79ae92e4c4a775929a06add99fcf3925f5fef82)", "zcash_transparent", "zebra-test", ] @@ -6852,6 +6898,7 @@ dependencies = [ "tracing-subscriber", "zcash_proofs", "zcash_protocol", + "zcash_tachyon 0.0.0 (git+https://github.com/S1nus/tachyon.git?rev=f79ae92e4c4a775929a06add99fcf3925f5fef82)", "zebra-chain", "zebra-node-services", "zebra-script", diff --git a/Cargo.toml b/Cargo.toml index c4de1ca0d18..8f3f07c58e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,16 +21,18 @@ resolver = "2" [workspace.dependencies] incrementalmerkletree = { version = "0.8.2", features = ["legacy-api"] } -orchard = "0.11" -sapling-crypto = "0.5" -zcash_address = "0.10" -zcash_encoding = "0.3" -zcash_history = "0.4" -zcash_keys = "0.12" -zcash_primitives = "0.26" -zcash_proofs = "0.26" -zcash_transparent = "0.6" -zcash_protocol = "0.7" +orchard = "0.12" +zcash_tachyon = {git = "https://github.com/S1nus/tachyon.git", rev="f79ae92e4c4a775929a06add99fcf3925f5fef82"} +sapling-crypto = "0.6" +zcash_address = {git = "https://github.com/tachyon-zcash/librustzcash.git", branch = "main"} +zcash_encoding = {git = "https://github.com/tachyon-zcash/librustzcash.git", branch = "main"} +zcash_history = {git = "https://github.com/tachyon-zcash/librustzcash.git", branch = "main"} +zcash_keys = {git = "https://github.com/tachyon-zcash/librustzcash.git", branch = "main"} +zcash_primitives = {git = "https://github.com/tachyon-zcash/librustzcash.git", branch = "main"} +zcash_proofs = {git = "https://github.com/tachyon-zcash/librustzcash.git", branch = "main"} +zcash_transparent = {git = "https://github.com/tachyon-zcash/librustzcash.git", branch = "main"} +zcash_protocol = {git = "https://github.com/tachyon-zcash/librustzcash.git", branch = "main"} +equihash = {git = "https://github.com/tachyon-zcash/librustzcash.git", branch = "main"} zip32 = "0.2" abscissa_core = "0.7" atty = "0.2.14" @@ -58,7 +60,6 @@ derive-new = "0.5" dirs = "6.0" ed25519-zebra = "4.0.3" elasticsearch = { version = "8.17.0-alpha.1", default-features = false } -equihash = "0.2.2" ff = "0.13" futures = "0.3.31" futures-core = "0.3.31" @@ -95,6 +96,7 @@ num-integer = "0.1.46" once_cell = "1.21" ordered-map = "0.4.2" owo-colors = "4.2.0" +pasta_curves = "0.5.1" pin-project = "1.1.10" primitive-types = "0.12" proptest = "1.6" diff --git a/zebra-chain/Cargo.toml b/zebra-chain/Cargo.toml index 6aea5c83dbe..7f6238fe1b2 100644 --- a/zebra-chain/Cargo.toml +++ b/zebra-chain/Cargo.toml @@ -82,8 +82,11 @@ bech32 = { workspace = true } zcash_script.workspace = true # ECC deps +ff.workspace = true +pasta_curves.workspace = true halo2 = { package = "halo2_proofs", version = "0.3" } orchard.workspace = true +zcash_tachyon = { workspace = true, features = ["serde"] } zcash_encoding.workspace = true zcash_history.workspace = true zcash_note_encryption = { workspace = true } diff --git a/zebra-chain/src/block/height.rs b/zebra-chain/src/block/height.rs index 71f664c75a9..1b9f789c317 100644 --- a/zebra-chain/src/block/height.rs +++ b/zebra-chain/src/block/height.rs @@ -2,7 +2,7 @@ use std::ops::{Add, Sub}; use thiserror::Error; -use zcash_primitives::consensus::BlockHeight; +use zcash_protocol::consensus::BlockHeight; use crate::{serialization::SerializationError, BoxError}; diff --git a/zebra-chain/src/parameters/network.rs b/zebra-chain/src/parameters/network.rs index 5c9e9ffbf31..168d4cff055 100644 --- a/zebra-chain/src/parameters/network.rs +++ b/zebra-chain/src/parameters/network.rs @@ -10,6 +10,7 @@ use crate::{ parameters::NetworkUpgrade, transparent, }; +use zcash_protocol::constants::{mainnet as mainnet_constants, testnet as testnet_constants}; mod error; pub mod magic; @@ -66,9 +67,9 @@ impl NetworkKind { /// pay-to-public-key-hash payment addresses for the network. pub fn b58_pubkey_address_prefix(self) -> [u8; 2] { match self { - Self::Mainnet => zcash_primitives::constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX, + Self::Mainnet => mainnet_constants::B58_PUBKEY_ADDRESS_PREFIX, Self::Testnet | Self::Regtest => { - zcash_primitives::constants::testnet::B58_PUBKEY_ADDRESS_PREFIX + testnet_constants::B58_PUBKEY_ADDRESS_PREFIX } } } @@ -77,9 +78,9 @@ impl NetworkKind { /// payment addresses for the network. pub fn b58_script_address_prefix(self) -> [u8; 2] { match self { - Self::Mainnet => zcash_primitives::constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX, + Self::Mainnet => mainnet_constants::B58_SCRIPT_ADDRESS_PREFIX, Self::Testnet | Self::Regtest => { - zcash_primitives::constants::testnet::B58_SCRIPT_ADDRESS_PREFIX + testnet_constants::B58_SCRIPT_ADDRESS_PREFIX } } } diff --git a/zebra-chain/src/parameters/network_upgrade.rs b/zebra-chain/src/parameters/network_upgrade.rs index 482b39a307f..8e83aede1f3 100644 --- a/zebra-chain/src/parameters/network_upgrade.rs +++ b/zebra-chain/src/parameters/network_upgrade.rs @@ -208,11 +208,11 @@ impl fmt::Display for ConsensusBranchId { } } -impl TryFrom for zcash_primitives::consensus::BranchId { +impl TryFrom for zcash_protocol::consensus::BranchId { type Error = crate::Error; fn try_from(id: ConsensusBranchId) -> Result { - zcash_primitives::consensus::BranchId::try_from(u32::from(id)) + zcash_protocol::consensus::BranchId::try_from(u32::from(id)) .map_err(|_| Self::Error::InvalidConsensusBranchId) } } diff --git a/zebra-chain/src/primitives/address.rs b/zebra-chain/src/primitives/address.rs index c0052f45f45..aa7023c5771 100644 --- a/zebra-chain/src/primitives/address.rs +++ b/zebra-chain/src/primitives/address.rs @@ -3,7 +3,7 @@ //! Usage: use zcash_address::unified::{self, Container}; -use zcash_primitives::consensus::NetworkType; +use zcash_protocol::consensus::NetworkType; use crate::{parameters::NetworkKind, transparent, BoxError}; diff --git a/zebra-chain/src/transaction.rs b/zebra-chain/src/transaction.rs index 6e6b9c240b0..efb380737e2 100644 --- a/zebra-chain/src/transaction.rs +++ b/zebra-chain/src/transaction.rs @@ -171,6 +171,8 @@ pub enum Transaction { sapling_shielded_data: Option>, /// The orchard data for this transaction, if any. orchard_shielded_data: Option, + /// The tachyon data for this transaction, if any. + tachyon_shielded_data: Option>>, }, } diff --git a/zebra-chain/src/transaction/builder.rs b/zebra-chain/src/transaction/builder.rs index d5d85a9aed8..9ffff04a74d 100644 --- a/zebra-chain/src/transaction/builder.rs +++ b/zebra-chain/src/transaction/builder.rs @@ -101,6 +101,7 @@ impl Transaction { // See the Zcash spec for additional shielded coinbase consensus rules. sapling_shielded_data: None, orchard_shielded_data: None, + tachyon_shielded_data: None, } } diff --git a/zebra-chain/src/transaction/serialize.rs b/zebra-chain/src/transaction/serialize.rs index e67df823fcf..f6354a2785b 100644 --- a/zebra-chain/src/transaction/serialize.rs +++ b/zebra-chain/src/transaction/serialize.rs @@ -4,7 +4,8 @@ use std::{borrow::Borrow, io, sync::Arc}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use halo2::pasta::group::ff::PrimeField; +use ff::PrimeField; +use group::GroupEncoding; use hex::FromHex; use reddsa::{orchard::Binding, orchard::SpendAuth, Signature}; @@ -686,6 +687,7 @@ impl ZcashSerialize for Transaction { outputs, sapling_shielded_data, orchard_shielded_data, + tachyon_shielded_data, } => { // Transaction V6 spec: // https://zips.z.cash/zip-0230#specification @@ -725,6 +727,8 @@ impl ZcashSerialize for Transaction { // `flagsOrchard`,`valueBalanceOrchard`, `anchorOrchard`, `sizeProofsOrchard`, // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. orchard_shielded_data.zcash_serialize(&mut writer)?; + + tachyon_shielded_data.zcash_serialize(&mut writer)?; } } Ok(()) @@ -1015,6 +1019,8 @@ impl ZcashDeserialize for Transaction { // `proofsOrchard`, `vSpendAuthSigsOrchard`, and `bindingSigOrchard`. let orchard_shielded_data = (&mut limited_reader).zcash_deserialize_into()?; + let tachyon_shielded_data = (&mut limited_reader).zcash_deserialize_into()?; + Ok(Transaction::V6 { network_upgrade, lock_time, @@ -1024,6 +1030,7 @@ impl ZcashDeserialize for Transaction { outputs, sapling_shielded_data, orchard_shielded_data, + tachyon_shielded_data }) } (_, _) => Err(SerializationError::Parse("bad tx header")), @@ -1173,3 +1180,298 @@ impl FromHex for SerializedTransaction { Ok(bytes.into()) } } + +// Tachyon Bundle serialization implementations +impl ZcashSerialize for Option> +where + S: ZcashSerialize + zcash_tachyon::bundle::StampState, + zcash_tachyon::Bundle: ZcashSerialize, +{ + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + match self { + None => { + writer.write_u8(0)?; + } + Some(tachyon_bundle) => { + writer.write_u8(1)?; + tachyon_bundle.zcash_serialize(&mut writer)?; + } + } + Ok(()) + } +} + +impl ZcashSerialize for zcash_tachyon::Bundle +where + S: ZcashSerialize + zcash_tachyon::bundle::StampState, +{ + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + // Denoted as `nActionsTachyon` and `vActionsTachyon` in the spec + self.actions.zcash_serialize(&mut writer)?; + + // Denoted as `valueBalanceTachyon` in the spec + writer.write_i64::(self.value_balance)?; + + // Denoted as `bindingSigTachyon` in the spec + self.binding_sig.zcash_serialize(&mut writer)?; + + // Denoted as stamp data (stamp present or stripped) + self.stamp.zcash_serialize(&mut writer)?; + + Ok(()) + } +} + +impl ZcashDeserialize for Option> +where + S: ZcashDeserialize + zcash_tachyon::bundle::StampState, +{ + fn zcash_deserialize(mut reader: R) -> Result { + // Read presence flag byte + let flag = (&mut reader).read_u8()?; + + match flag { + 0 => Ok(None), + 1 => { + let actions: Vec = + (&mut reader).zcash_deserialize_into()?; + let value_balance = (&mut reader).read_i64::()?; + let binding_sig = (&mut reader).zcash_deserialize_into()?; + let stamp: S = (&mut reader).zcash_deserialize_into()?; + + Ok(Some(zcash_tachyon::Bundle { + actions, + value_balance, + binding_sig, + stamp, + })) + } + _ => Err(SerializationError::Parse( + "invalid tachyon bundle presence flag: expected 0 or 1", + )), + } + } +} + +// Tachyon signature serializations +impl ZcashSerialize for zcash_tachyon::action::Signature { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&<[u8; 64]>::from(*self)[..]) + } +} + +impl ZcashDeserialize for zcash_tachyon::action::Signature { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(reader.read_64_bytes()?.into()) + } +} + +impl ZcashSerialize for zcash_tachyon::bundle::Signature { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + writer.write_all(&<[u8; 64]>::from(*self)[..]) + } +} + +impl ZcashDeserialize for zcash_tachyon::bundle::Signature { + fn zcash_deserialize(mut reader: R) -> Result { + Ok(reader.read_64_bytes()?.into()) + } +} + +// Tachyon TrustedPreallocate implementations +impl TrustedPreallocate for zcash_tachyon::Action { + // Action = 32 (cv) + 32 (rk) + 64 (sig) = 128 bytes + fn max_allocation() -> u64 { + (MAX_BLOCK_BYTES - 1) / 128 + } +} + +impl TrustedPreallocate for zcash_tachyon::Tachygram { + // Tachygram = 32 bytes (Fp field element) + fn max_allocation() -> u64 { + (MAX_BLOCK_BYTES - 1) / 32 + } +} + +// Individual tachyon component serializations +impl ZcashSerialize for zcash_tachyon::Action { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + // Serialize cv (value commitment) + self.cv.zcash_serialize(&mut writer)?; + // Serialize rk (randomized verification key) + self.rk.zcash_serialize(&mut writer)?; + // Serialize sig (spend auth signature) + self.sig.zcash_serialize(&mut writer)?; + Ok(()) + } +} + +impl ZcashDeserialize for zcash_tachyon::Action { + fn zcash_deserialize(mut reader: R) -> Result { + let cv = (&mut reader).zcash_deserialize_into()?; + let rk = (&mut reader).zcash_deserialize_into()?; + let sig = (&mut reader).zcash_deserialize_into()?; + + Ok(zcash_tachyon::Action { cv, rk, sig }) + } +} + +impl ZcashSerialize for zcash_tachyon::Stamp { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + // Serialize tachygrams list + self.tachygrams.zcash_serialize(&mut writer)?; + // Serialize anchor + self.anchor.zcash_serialize(&mut writer)?; + // Serialize proof + self.proof.zcash_serialize(&mut writer)?; + Ok(()) + } +} + +impl ZcashDeserialize for zcash_tachyon::Stamp { + fn zcash_deserialize(mut reader: R) -> Result { + let tachygrams = (&mut reader).zcash_deserialize_into()?; + let anchor = (&mut reader).zcash_deserialize_into()?; + let proof = (&mut reader).zcash_deserialize_into()?; + + Ok(zcash_tachyon::Stamp { + tachygrams, + anchor, + proof, + }) + } +} + +impl ZcashSerialize for zcash_tachyon::stamp::Stampless { + fn zcash_serialize(&self, _writer: W) -> Result<(), io::Error> { + // Stampless is a zero-sized marker type, nothing to serialize + Ok(()) + } +} + +impl ZcashDeserialize for zcash_tachyon::stamp::Stampless { + fn zcash_deserialize(_reader: R) -> Result { + // Stampless is a zero-sized marker type, nothing to deserialize + Ok(zcash_tachyon::stamp::Stampless) + } +} + +impl ZcashSerialize for Option { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + match self { + None => writer.write_u8(0)?, + Some(stamp) => { + writer.write_u8(1)?; + stamp.zcash_serialize(&mut writer)?; + } + } + Ok(()) + } +} + +impl ZcashDeserialize for Option { + fn zcash_deserialize(mut reader: R) -> Result { + let flag = reader.read_u8()?; + match flag { + 0 => Ok(None), + 1 => Ok(Some(zcash_tachyon::Stamp::zcash_deserialize(&mut reader)?)), + _ => Err(SerializationError::Parse("invalid tachyon stamp flag")), + } + } +} + +// Individual primitive serializations +impl ZcashSerialize for zcash_tachyon::Tachygram { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + let fp: pasta_curves::Fp = (*self).into(); + let bytes = fp.to_repr(); + writer.write_all(&bytes) + } +} + +impl ZcashDeserialize for zcash_tachyon::Tachygram { + fn zcash_deserialize(mut reader: R) -> Result { + let bytes = reader.read_32_bytes()?; + let fp_option = pasta_curves::Fp::from_repr(bytes); + if fp_option.is_some().into() { + Ok(fp_option.unwrap().into()) + } else { + Err(SerializationError::Parse("invalid tachygram field element")) + } + } +} + +impl ZcashSerialize for zcash_tachyon::Anchor { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + let fp: pasta_curves::Fp = (*self).into(); + let bytes = fp.to_repr(); + writer.write_all(&bytes) + } +} + +impl ZcashDeserialize for zcash_tachyon::Anchor { + fn zcash_deserialize(mut reader: R) -> Result { + let bytes = reader.read_32_bytes()?; + let fp_option = pasta_curves::Fp::from_repr(bytes); + if fp_option.is_some().into() { + Ok(fp_option.unwrap().into()) + } else { + Err(SerializationError::Parse("invalid anchor field element")) + } + } +} + +/// Serialized size of a Tachyon proof in bytes. +/// Matches `mock_ragu::proof::PROOF_SIZE_COMPRESSED`. +const TACHYON_PROOF_SIZE: usize = 23_000; + +impl ZcashSerialize for zcash_tachyon::Proof { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + let bytes = self.serialize(); + writer.write_all(bytes.as_ref()) + } +} + +impl ZcashDeserialize for zcash_tachyon::Proof { + fn zcash_deserialize(mut reader: R) -> Result { + let mut bytes = vec![0u8; TACHYON_PROOF_SIZE]; + reader.read_exact(&mut bytes)?; + let arr: [u8; TACHYON_PROOF_SIZE] = bytes.try_into().expect("vec is TACHYON_PROOF_SIZE"); + zcash_tachyon::Proof::try_from(&arr) + .map_err(|_| SerializationError::Parse("invalid tachyon proof")) + } +} + +impl ZcashSerialize for zcash_tachyon::value::Commitment { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + let point: pasta_curves::EpAffine = (*self).into(); + let bytes = point.to_bytes(); + writer.write_all(&bytes) + } +} + +impl ZcashDeserialize for zcash_tachyon::value::Commitment { + fn zcash_deserialize(mut reader: R) -> Result { + let bytes = reader.read_32_bytes()?; + let point_option = pasta_curves::EpAffine::from_bytes(&bytes); + if point_option.is_some().into() { + Ok(point_option.unwrap().into()) + } else { + Err(SerializationError::Parse("invalid value commitment point")) + } + } +} + +impl ZcashSerialize for zcash_tachyon::keys::public::ActionVerificationKey { + fn zcash_serialize(&self, mut writer: W) -> Result<(), io::Error> { + let bytes: [u8; 32] = (*self).into(); + writer.write_all(&bytes) + } +} + +impl ZcashDeserialize for zcash_tachyon::keys::public::ActionVerificationKey { + fn zcash_deserialize(mut reader: R) -> Result { + let bytes = reader.read_32_bytes()?; + Self::try_from(bytes).map_err(|_| SerializationError::Parse("invalid action verification key")) + } +} diff --git a/zebra-chain/src/transaction/tests/vectors.rs b/zebra-chain/src/transaction/tests/vectors.rs index 1d67a5a5525..7ac42e3be84 100644 --- a/zebra-chain/src/transaction/tests/vectors.rs +++ b/zebra-chain/src/transaction/tests/vectors.rs @@ -1032,3 +1032,227 @@ fn test_coinbase_script() -> Result<()> { Ok(()) } + +// Transaction V6 test vectors + +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +mod v6_tests { + use super::*; + use group::ff::FromUniformBytes; + use group::ff::PrimeField; + use group::prime::PrimeCurveAffine; + use halo2::pasta::pallas; + use reddsa::{orchard::SpendAuth, SigningKey, VerificationKey}; + + /// Helper: derive a valid ActionVerificationKey from a 64-byte seed. + fn rk_from_seed(seed: [u8; 64]) -> zcash_tachyon::keys::public::ActionVerificationKey { + let sk_scalar = pallas::Scalar::from_uniform_bytes(&seed); + let sk_bytes = sk_scalar.to_repr(); + let sk = SigningKey::::try_from(sk_bytes).unwrap(); + let pk = VerificationKey::::from(&sk); + zcash_tachyon::keys::public::ActionVerificationKey::try_from(<[u8; 32]>::from(pk)).unwrap() + } + + /// Helper: derive Fp from a 64-byte seed. + fn fp_from_seed(seed: [u8; 64]) -> pasta_curves::Fp { + pasta_curves::Fp::from_uniform_bytes(&seed) + } + + lazy_static! { + /// An empty V6 transaction with no bundles at all. + pub static ref EMPTY_V6_TX: Transaction = Transaction::V6 { + network_upgrade: NetworkUpgrade::Nu7, + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: block::Height(0), + zip233_amount: Amount::try_from(0).unwrap(), + inputs: Vec::new(), + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + tachyon_shielded_data: None, + }; + + /// V6 transaction with a tachyon bundle, stamp = None. + pub static ref V6_TX_TACHYON_NO_STAMP: Transaction = { + let rk = rk_from_seed([0x42u8; 64]); + let action = zcash_tachyon::Action { + cv: zcash_tachyon::value::Commitment::from(pasta_curves::EpAffine::generator()), + rk, + sig: zcash_tachyon::action::Signature::from([0x01u8; 64]), + }; + Transaction::V6 { + network_upgrade: NetworkUpgrade::Nu7, + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: block::Height(0), + zip233_amount: Amount::try_from(0).unwrap(), + inputs: Vec::new(), + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + tachyon_shielded_data: Some(zcash_tachyon::Bundle { + actions: vec![action], + value_balance: 0i64, + binding_sig: zcash_tachyon::bundle::Signature::from([0x02u8; 64]), + stamp: None, + }), + } + }; + + /// V6 transaction with a tachyon bundle that includes a stamp with tachygrams. + pub static ref V6_TX_TACHYON_WITH_STAMP: Transaction = { + let rk = rk_from_seed([0x42u8; 64]); + let action = zcash_tachyon::Action { + cv: zcash_tachyon::value::Commitment::from(pasta_curves::EpAffine::generator()), + rk, + sig: zcash_tachyon::action::Signature::from([0x01u8; 64]), + }; + let tachygram = zcash_tachyon::Tachygram::from(fp_from_seed([0xAAu8; 64])); + let anchor = zcash_tachyon::Anchor::from(fp_from_seed([0xBBu8; 64])); + Transaction::V6 { + network_upgrade: NetworkUpgrade::Nu7, + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: block::Height(0), + zip233_amount: Amount::try_from(0).unwrap(), + inputs: Vec::new(), + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + tachyon_shielded_data: Some(zcash_tachyon::Bundle { + actions: vec![action], + value_balance: 100i64, + binding_sig: zcash_tachyon::bundle::Signature::from([0x02u8; 64]), + stamp: Some(zcash_tachyon::Stamp { + tachygrams: vec![tachygram], + anchor, + proof: zcash_tachyon::Proof, + }), + }), + } + }; + + /// V6 transaction with multiple actions and multiple tachygrams. + pub static ref V6_TX_TACHYON_MULTI_ACTION: Transaction = { + let rk1 = rk_from_seed([0x42u8; 64]); + let rk2 = rk_from_seed([0x43u8; 64]); + let action1 = zcash_tachyon::Action { + cv: zcash_tachyon::value::Commitment::from(pasta_curves::EpAffine::generator()), + rk: rk1, + sig: zcash_tachyon::action::Signature::from([0x01u8; 64]), + }; + let action2 = zcash_tachyon::Action { + cv: zcash_tachyon::value::Commitment::from(pasta_curves::EpAffine::generator()), + rk: rk2, + sig: zcash_tachyon::action::Signature::from([0x03u8; 64]), + }; + let tg1 = zcash_tachyon::Tachygram::from(fp_from_seed([0xAAu8; 64])); + let tg2 = zcash_tachyon::Tachygram::from(fp_from_seed([0xCCu8; 64])); + let tg3 = zcash_tachyon::Tachygram::from(fp_from_seed([0xDDu8; 64])); + let anchor = zcash_tachyon::Anchor::from(fp_from_seed([0xBBu8; 64])); + Transaction::V6 { + network_upgrade: NetworkUpgrade::Nu7, + lock_time: LockTime::min_lock_time_timestamp(), + expiry_height: block::Height(0), + zip233_amount: Amount::try_from(500).unwrap(), + inputs: Vec::new(), + outputs: Vec::new(), + sapling_shielded_data: None, + orchard_shielded_data: None, + tachyon_shielded_data: Some(zcash_tachyon::Bundle { + actions: vec![action1, action2], + value_balance: 300i64, + binding_sig: zcash_tachyon::bundle::Signature::from([0x02u8; 64]), + stamp: Some(zcash_tachyon::Stamp { + tachygrams: vec![tg1, tg2, tg3], + anchor, + proof: zcash_tachyon::Proof, + }), + }), + } + }; + } + + /// An empty V6 transaction round-trip test. + #[test] + fn empty_v6_round_trip() { + let _init_guard = zebra_test::init(); + + let tx: &Transaction = &EMPTY_V6_TX; + + let data = tx.zcash_serialize_to_vec().expect("tx should serialize"); + let tx2: &Transaction = &data + .zcash_deserialize_into() + .expect("tx should deserialize"); + + assert_eq!(tx, tx2); + + let data2 = tx2 + .zcash_serialize_to_vec() + .expect("vec serialization is infallible"); + + assert_eq!(data, data2, "data must be equal if structs are equal"); + } + + /// Generate and print V6 tachyon test vectors as hex. + #[test] + fn generate_v6_tachyon_test_vectors() { + let _init_guard = zebra_test::init(); + + let test_cases: &[(&str, &Transaction)] = &[ + ("EMPTY_V6_TX", &EMPTY_V6_TX), + ("V6_TX_TACHYON_NO_STAMP", &V6_TX_TACHYON_NO_STAMP), + ("V6_TX_TACHYON_WITH_STAMP", &V6_TX_TACHYON_WITH_STAMP), + ("V6_TX_TACHYON_MULTI_ACTION", &V6_TX_TACHYON_MULTI_ACTION), + ]; + + for (name, tx) in test_cases { + let bytes = tx + .zcash_serialize_to_vec() + .expect("tx should serialize"); + + let tx2: Transaction = bytes + .zcash_deserialize_into() + .expect("tx should deserialize"); + + assert_eq!(*tx, &tx2, "{name} round-trip failed"); + + println!("{name}: {}", hex::encode(&bytes)); + } + } + + /// Assert that V6 tachyon test vectors serialize to exact expected bytes. + #[test] + fn v6_tachyon_test_vectors_exact_encoding() { + let _init_guard = zebra_test::init(); + + let test_cases: &[(&str, &Transaction, &str)] = &[ + ( + "EMPTY_V6_TX", + &EMPTY_V6_TX, + "06000080ffffffffffffffff0065cd1d000000000000000000000000000000000000", + ), + ( + "V6_TX_TACHYON_NO_STAMP", + &V6_TX_TACHYON_NO_STAMP, + "06000080ffffffffffffffff0065cd1d0000000000000000000000000000000000010100000000ed302d991bf94c09fc98462200000000000000000000000000000040ba6454c4a1d42730b53cbf30d05d3f95aa541c98eba0205a75bb7983443b37310101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100000000000000000202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200", + ), + ( + "V6_TX_TACHYON_WITH_STAMP", + &V6_TX_TACHYON_WITH_STAMP, + "06000080ffffffffffffffff0065cd1d0000000000000000000000000000000000010100000000ed302d991bf94c09fc98462200000000000000000000000000000040ba6454c4a1d42730b53cbf30d05d3f95aa541c98eba0205a75bb7983443b37310101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010164000000000000000202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201015f555555c6580ae6f8e822b5273ca4f06593dbd767c60fa50d7a6852ca2b9e1b4f44444458b35c8c947f94ae11ebee5823220b073f5a913542860cc19196c724", + ), + ( + "V6_TX_TACHYON_MULTI_ACTION", + &V6_TX_TACHYON_MULTI_ACTION, + "06000080ffffffffffffffff0065cd1d00000000f4010000000000000000000000010200000000ed302d991bf94c09fc98462200000000000000000000000000000040ba6454c4a1d42730b53cbf30d05d3f95aa541c98eba0205a75bb7983443b37310101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100000000ed302d991bf94c09fc98462200000000000000000000000000000040336a1f7ed0903193f39fa5306f3fd88d8a0a8907a1defde547f117e7075d9e01030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303032c010000000000000202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201035f555555c6580ae6f8e822b5273ca4f06593dbd767c60fa50d7a6852ca2b9e1b3f333333ea0daf32301606a8fb9939c1e0b03a3616ee12c67692b02f5901f12d2f2222227c6801d9cbac77a1e54884299e3f6a65ed819456ab9e549e206c1a374f44444458b35c8c947f94ae11ebee5823220b073f5a913542860cc19196c724", + ), + ]; + + for (name, tx, expected_hex) in test_cases { + let bytes = tx + .zcash_serialize_to_vec() + .expect("tx should serialize"); + + assert_eq!(hex::encode(&bytes), *expected_hex, "{name} encoding mismatch"); + } + } +} diff --git a/zebra-chain/src/transparent/address.rs b/zebra-chain/src/transparent/address.rs index 8ab18caf0b1..7010db55db0 100644 --- a/zebra-chain/src/transparent/address.rs +++ b/zebra-chain/src/transparent/address.rs @@ -11,6 +11,7 @@ use crate::{ #[cfg(test)] use proptest::prelude::*; use zcash_address::{ToAddress, ZcashAddress}; +use zcash_protocol::constants::{mainnet as mainnet_constants, testnet as testnet_constants}; /// Transparent Zcash Addresses /// @@ -142,12 +143,12 @@ impl std::str::FromStr for Address { hash_bytes.copy_from_slice(&payload); match hrp.as_str() { - zcash_primitives::constants::mainnet::HRP_TEX_ADDRESS => Ok(Address::Tex { + mainnet_constants::HRP_TEX_ADDRESS => Ok(Address::Tex { network_kind: NetworkKind::Mainnet, validating_key_hash: hash_bytes, }), - zcash_primitives::constants::testnet::HRP_TEX_ADDRESS => Ok(Address::Tex { + testnet_constants::HRP_TEX_ADDRESS => Ok(Address::Tex { network_kind: NetworkKind::Testnet, validating_key_hash: hash_bytes, }), @@ -196,25 +197,25 @@ impl ZcashDeserialize for Address { reader.read_exact(&mut hash_bytes)?; match version_bytes { - zcash_primitives::constants::mainnet::B58_SCRIPT_ADDRESS_PREFIX => { + mainnet_constants::B58_SCRIPT_ADDRESS_PREFIX => { Ok(Address::PayToScriptHash { network_kind: NetworkKind::Mainnet, script_hash: hash_bytes, }) } - zcash_primitives::constants::testnet::B58_SCRIPT_ADDRESS_PREFIX => { + testnet_constants::B58_SCRIPT_ADDRESS_PREFIX => { Ok(Address::PayToScriptHash { network_kind: NetworkKind::Testnet, script_hash: hash_bytes, }) } - zcash_primitives::constants::mainnet::B58_PUBKEY_ADDRESS_PREFIX => { + mainnet_constants::B58_PUBKEY_ADDRESS_PREFIX => { Ok(Address::PayToPublicKeyHash { network_kind: NetworkKind::Mainnet, pub_key_hash: hash_bytes, }) } - zcash_primitives::constants::testnet::B58_PUBKEY_ADDRESS_PREFIX => { + testnet_constants::B58_PUBKEY_ADDRESS_PREFIX => { Ok(Address::PayToPublicKeyHash { network_kind: NetworkKind::Testnet, pub_key_hash: hash_bytes, diff --git a/zebra-consensus/Cargo.toml b/zebra-consensus/Cargo.toml index bcd11109d5c..a539a1eedd9 100644 --- a/zebra-consensus/Cargo.toml +++ b/zebra-consensus/Cargo.toml @@ -24,7 +24,7 @@ progress-bar = [ "zebra-state/progress-bar", ] -tx_v6 = ["zebra-chain/tx_v6", "zebra-state/tx_v6"] +tx_v6 = ["zebra-chain/tx_v6", "zebra-state/tx_v6", "dep:zcash_tachyon"] # Test-only features proptest-impl = ["proptest", "proptest-derive", "zebra-chain/proptest-impl", "zebra-state/proptest-impl"] @@ -67,6 +67,7 @@ zebra-node-services = { path = "../zebra-node-services", version = "2.1.1" } zebra-chain = { path = "../zebra-chain", version = "3.1.0" } zcash_protocol.workspace = true +zcash_tachyon = { workspace = true, optional = true } # prod feature progress-bar howudoin = { workspace = true, optional = true } diff --git a/zebra-consensus/src/block.rs b/zebra-consensus/src/block.rs index fb0d7a3a00e..d43fe433ed8 100644 --- a/zebra-consensus/src/block.rs +++ b/zebra-consensus/src/block.rs @@ -227,6 +227,9 @@ where .map_err(VerifyBlockError::Time)?; let coinbase_tx = check::coinbase_is_first(&block)?; + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + let tachyon_checks = check::verify_tachyon_aggregates(&block)?; + let expected_block_subsidy = zebra_chain::parameters::subsidy::block_subsidy(height, &network)?; @@ -296,6 +299,17 @@ where } } + // Await tachyon batch verification futures. + #[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] + { + use futures::stream::{FuturesUnordered, StreamExt as _}; + let mut tachyon_async: FuturesUnordered<_> = + tachyon_checks.into_iter().collect(); + while let Some(result) = tachyon_async.next().await { + result.map_err(|e| BlockError::Other(e.to_string()))?; + } + } + // Check the summed block totals if sigops > MAX_BLOCK_SIGOPS { diff --git a/zebra-consensus/src/block/check.rs b/zebra-consensus/src/block/check.rs index 9015d25d7a9..4e91c4baf06 100644 --- a/zebra-consensus/src/block/check.rs +++ b/zebra-consensus/src/block/check.rs @@ -68,6 +68,92 @@ pub fn coinbase_is_first(block: &Block) -> Result, Ok(first.clone()) } +/// Verifies tachyon proof aggregation for a block. +/// +/// Tachyon transactions in a block use proof aggregation: a stamped bundle's +/// proof covers its own actions plus those of any immediately following +/// unstamped (stripped) bundles. This function enforces that structure and +/// queues each aggregate proof for batch verification. +/// +/// Returns futures that must be awaited to complete verification. +/// +/// Algorithm: +/// 1. Iterate through block transactions. +/// 2. For stamped tachyon txs: collect actions from self + following unstamped txs, queue proof. +/// 3. For unstamped tachyon txs not preceded by a stamped tx: error. +/// 4. Skip non-tachyon transactions. +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub fn verify_tachyon_aggregates( + block: &Block, +) -> Result>>, BlockError> { + use futures::FutureExt as _; + use tower::ServiceExt as _; + + let mut i = 0; + let txs = &block.transactions; + let mut checks: Vec>> = + Vec::new(); + + while i < txs.len() { + // Extract tachyon bundle from this transaction, if any. + let bundle = match txs[i].as_ref() { + Transaction::V6 { + tachyon_shielded_data: Some(bundle), + .. + } => bundle, + _ => { + i += 1; + continue; + } + }; + + match &bundle.stamp { + Some(stamp) => { + // Stamped bundle: collect actions from this tx and following unstamped txs. + let mut all_actions: Vec = bundle.actions.clone(); + + // Advance past this stamped tx and collect trailing unstamped tachyon txs. + i += 1; + while i < txs.len() { + match txs[i].as_ref() { + Transaction::V6 { + tachyon_shielded_data: Some(adj_bundle), + .. + } if adj_bundle.stamp.is_none() => { + all_actions.extend(adj_bundle.actions.iter().cloned()); + i += 1; + } + _ => break, + } + } + + // Build the multiset and queue the stamp proof for batch verification. + let actions_multiset = zcash_tachyon::multiset::Multiset::try_from( + all_actions.as_slice(), + ) + .map_err(|_| BlockError::Other("invalid tachyon action digest".to_string()))?; + + let item = + crate::primitives::tachyon::Item::new(stamp.clone(), actions_multiset); + checks.push( + crate::primitives::tachyon::VERIFIER + .clone() + .oneshot(item) + .boxed(), + ); + } + None => { + // Unstamped tachyon tx not covered by a preceding stamped tx. + return Err(BlockError::Other( + "unstamped tachyon bundle not preceded by a stamped bundle".to_string(), + )); + } + } + } + + Ok(checks) +} + /// Returns `Ok(ExpandedDifficulty)` if the`difficulty_threshold` of `header` is at least as difficult as /// the target difficulty limit for `network` (PoWLimit) /// diff --git a/zebra-consensus/src/lib.rs b/zebra-consensus/src/lib.rs index 9cda93b8470..1937455bc64 100644 --- a/zebra-consensus/src/lib.rs +++ b/zebra-consensus/src/lib.rs @@ -55,6 +55,8 @@ pub use checkpoint::{VerifyCheckpointError, MAX_CHECKPOINT_BYTE_COUNT, MAX_CHECK pub use config::Config; pub use error::BlockError; pub use primitives::{ed25519, groth16, halo2, redjubjub, redpallas}; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub use primitives::tachyon; pub use router::RouterError; /// A boxed [`std::error::Error`]. diff --git a/zebra-consensus/src/primitives.rs b/zebra-consensus/src/primitives.rs index 822349e18d1..81d49008010 100644 --- a/zebra-consensus/src/primitives.rs +++ b/zebra-consensus/src/primitives.rs @@ -11,6 +11,9 @@ pub mod redjubjub; pub mod redpallas; pub mod sapling; +#[cfg(all(zcash_unstable = "nu7", feature = "tx_v6"))] +pub mod tachyon; + /// The maximum batch size for any of the batch verifiers. const MAX_BATCH_SIZE: usize = 64; diff --git a/zebra-consensus/src/primitives/tachyon.rs b/zebra-consensus/src/primitives/tachyon.rs new file mode 100644 index 00000000000..4718b72c782 --- /dev/null +++ b/zebra-consensus/src/primitives/tachyon.rs @@ -0,0 +1,262 @@ +//! Async Tachyon batch verifier service + +use std::{ + fmt, + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use futures::{future::BoxFuture, FutureExt}; +use once_cell::sync::Lazy; +use rand::thread_rng; +use zcash_tachyon::{ActionDigest, BatchValidator, Multiset, Stamp}; + +use crate::BoxError; +use thiserror::Error; +use tokio::sync::watch; +use tower::{util::ServiceFn, Service}; +use tower_batch_control::{Batch, BatchControl, RequestWeight}; +use tower_fallback::Fallback; + +use super::spawn_fifo; + +/// Adjusted batch size for tachyon batches. +const TACHYON_MAX_BATCH_SIZE: usize = super::MAX_BATCH_SIZE; + +/// The type of verification results. +type VerifyResult = bool; + +/// The type of the batch sender channel. +type Sender = watch::Sender>; + +/// A Tachyon verification item, used as the request type of the service. +#[derive(Clone, Debug)] +pub struct Item { + stamp: Stamp, + actions_multiset: Multiset, +} + +impl RequestWeight for Item { + fn request_weight(&self) -> usize { + 1 + } +} + +impl Item { + /// Creates a new [`Item`] from a stamp and actions multiset. + pub fn new(stamp: Stamp, actions_multiset: Multiset) -> Self { + Self { + stamp, + actions_multiset, + } + } + + /// Perform non-batched verification of this [`Item`]. + /// + /// This is useful (in combination with `Item::clone`) for implementing + /// fallback logic when batch verification fails. + pub fn verify_single(self) -> bool { + let mut batch = BatchValidator::new(); + batch.queue(self); + batch.validate(thread_rng()) + } +} + +trait QueueBatchVerify { + fn queue(&mut self, item: Item); +} + +impl QueueBatchVerify for BatchValidator { + fn queue( + &mut self, + Item { + stamp, + actions_multiset, + }: Item, + ) { + self.add_bundle(stamp, actions_multiset); + } +} + +/// An error that may occur when verifying Tachyon stamp proofs. +#[derive(Clone, Debug, Error, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum TachyonError { + #[error("tachyon stamp verification failed")] + VerificationFailed, +} + +/// Global batch verification context for Tachyon stamp proofs. +/// +/// This service transparently batches contemporaneous proof verifications, +/// handling batch failures by falling back to individual verification. +/// +/// Note that making a `Service` call requires mutable access to the service, so +/// you should call `.clone()` on the global handle to create a local, mutable +/// handle. +pub static VERIFIER: Lazy< + Fallback< + Batch, + ServiceFn BoxFuture<'static, Result<(), BoxError>>>, + >, +> = Lazy::new(|| { + Fallback::new( + Batch::new( + Verifier::new(), + TACHYON_MAX_BATCH_SIZE, + None, + super::MAX_BATCH_LATENCY, + ), + // We want to fallback to individual verification if batch verification fails, + // so we need a Service to use. + tower::service_fn( + (|item: Item| Verifier::verify_single_spawning(item).boxed()) as fn(_) -> _, + ), + ) +}); + +/// Tachyon stamp proof verifier implementation +/// +/// This is the core implementation for the batch verification logic of the +/// Tachyon verifier. It handles batching incoming requests, driving batches to +/// completion, and reporting results. +pub struct Verifier { + /// The synchronous Tachyon batch validator. + batch: BatchValidator, + + /// A channel for broadcasting the result of a batch to the futures for each batch item. + /// + /// Each batch gets a newly created channel, so there is only ever one result sent per channel. + /// Tokio doesn't have a oneshot multi-consumer channel, so we use a watch channel. + tx: Sender, +} + +impl Verifier { + fn new() -> Self { + let batch = BatchValidator::new(); + let (tx, _) = watch::channel(None); + Self { batch, tx } + } + + /// Returns the batch verifier and channel sender from `self`, + /// replacing them with a new empty batch. + fn take(&mut self) -> (BatchValidator, Sender) { + // Use a new verifier and channel for each batch. + let batch = mem::take(&mut self.batch); + + let (tx, _) = watch::channel(None); + let tx = mem::replace(&mut self.tx, tx); + + (batch, tx) + } + + /// Synchronously process the batch, and send the result using the channel sender. + /// This function blocks until the batch is completed. + fn verify(batch: BatchValidator, tx: Sender) { + let result = batch.validate(thread_rng()); + let _ = tx.send(Some(result)); + } + + /// Flush the batch using a thread pool, and return the result via the channel. + /// This returns immediately, usually before the batch is completed. + fn flush_blocking(&mut self) { + let (batch, tx) = self.take(); + + // Correctness: Do CPU-intensive work on a dedicated thread, to avoid blocking other futures. + // + // We don't care about execution order here, because this method is only called on drop. + tokio::task::block_in_place(|| rayon::spawn_fifo(|| Self::verify(batch, tx))); + } + + /// Flush the batch using a thread pool, and return the result via the channel. + /// This function returns a future that becomes ready when the batch is completed. + async fn flush_spawning(batch: BatchValidator, tx: Sender) { + // Correctness: Do CPU-intensive work on a dedicated thread, to avoid blocking other futures. + let _ = tx.send( + spawn_fifo(move || batch.validate(thread_rng())) + .await + .ok(), + ); + } + + /// Verify a single item using a thread pool, and return the result. + async fn verify_single_spawning(item: Item) -> Result<(), BoxError> { + // Correctness: Do CPU-intensive work on a dedicated thread, to avoid blocking other futures. + if spawn_fifo(move || item.verify_single()).await? { + Ok(()) + } else { + Err("could not validate tachyon stamp proof".into()) + } + } +} + +impl fmt::Debug for Verifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = "Verifier"; + f.debug_struct(name) + .field("batch", &"..") + .field("tx", &self.tx) + .finish() + } +} + +impl Service> for Verifier { + type Response = (); + type Error = BoxError; + type Future = Pin> + Send + 'static>>; + + fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: BatchControl) -> Self::Future { + match req { + BatchControl::Item(item) => { + tracing::trace!("got item"); + self.batch.queue(item); + let mut rx = self.tx.subscribe(); + Box::pin(async move { + match rx.changed().await { + Ok(()) => { + // We use a new channel for each batch, + // so we always get the correct batch result here. + let is_valid = *rx + .borrow() + .as_ref() + .ok_or("threadpool unexpectedly dropped response channel sender. Is Zebra shutting down?")?; + + if is_valid { + tracing::trace!(?is_valid, "verified tachyon stamp proof"); + metrics::counter!("proofs.tachyon.verified").increment(1); + Ok(()) + } else { + tracing::trace!(?is_valid, "invalid tachyon stamp proof"); + metrics::counter!("proofs.tachyon.invalid").increment(1); + Err("could not validate tachyon stamp proofs".into()) + } + } + Err(_recv_error) => panic!("verifier was dropped without flushing"), + } + }) + } + + BatchControl::Flush => { + tracing::trace!("got tachyon flush command"); + + let (batch, tx) = self.take(); + + Box::pin(Self::flush_spawning(batch, tx).map(Ok)) + } + } + } +} + +impl Drop for Verifier { + fn drop(&mut self) { + // We need to flush the current batch in case there are still any pending futures. + // This returns immediately, usually before the batch is completed. + self.flush_blocking() + } +} diff --git a/zebra-rpc/src/methods.rs b/zebra-rpc/src/methods.rs index 9264593b534..8c2d3b12273 100644 --- a/zebra-rpc/src/methods.rs +++ b/zebra-rpc/src/methods.rs @@ -58,7 +58,7 @@ use tower::ServiceExt; use tracing::Instrument; use zcash_address::{unified::Encoding, TryFromAddress}; -use zcash_primitives::consensus::Parameters; +use zcash_protocol::consensus::Parameters; use zebra_chain::{ amount::{self, Amount, NegativeAllowed, NonNegative}, @@ -1823,7 +1823,7 @@ where let time = u32::try_from(block.header.time.timestamp()) .expect("Timestamps of valid blocks always fit into u32."); - let sapling_nu = zcash_primitives::consensus::NetworkUpgrade::Sapling; + let sapling_nu = zcash_protocol::consensus::NetworkUpgrade::Sapling; let sapling = if network.is_nu_active(sapling_nu, height.into()) { match read_state .ready() @@ -1844,7 +1844,7 @@ where let (sapling_tree, sapling_root) = sapling.map_or((None, None), |(tree, root)| (Some(tree), Some(root))); - let orchard_nu = zcash_primitives::consensus::NetworkUpgrade::Nu5; + let orchard_nu = zcash_protocol::consensus::NetworkUpgrade::Nu5; let orchard = if network.is_nu_active(orchard_nu, height.into()) { match read_state .ready()