diff --git a/Cargo.lock b/Cargo.lock index a0390d68d1..ad292ce9d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -316,6 +316,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -754,6 +763,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -853,6 +868,12 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.12" @@ -1410,6 +1431,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "enum-ordinalize" version = "3.1.15" @@ -1746,6 +1779,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getset" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1858,6 +1903,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1889,6 +1943,20 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -2700,14 +2768,14 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "orchard" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f18e997fa121de5c73e95cdc7e8512ae43b7de38904aeea5e5713cc48f3c0ba" +source = "git+https://github.com/zcash/orchard.git?rev=bcd08e1d23e70c42a338f3e3f79d6f4c0c219805#bcd08e1d23e70c42a338f3e3f79d6f4c0c219805" dependencies = [ "aes", "bitvec", "blake2b_simd", "ff", "fpe", + "getset", "group", "halo2_gadgets", "halo2_proofs", @@ -2865,6 +2933,30 @@ dependencies = [ [[package]] name = "pczt" version = "0.0.0" +dependencies = [ + "blake2b_simd", + "bls12_381", + "ff", + "getset", + "incrementalmerkletree", + "jubjub", + "nonempty", + "orchard", + "pasta_curves", + "postcard", + "rand_core", + "redjubjub", + "sapling-crypto", + "secp256k1", + "serde", + "serde_with", + "shardtree", + "zcash_note_encryption", + "zcash_primitives", + "zcash_proofs", + "zcash_protocol", + "zip32", +] [[package]] name = "pem-rfc7468" @@ -3046,6 +3138,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "postcard" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3130,6 +3235,28 @@ dependencies = [ "toml_edit 0.19.15", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.63", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -3671,8 +3798,7 @@ dependencies = [ [[package]] name = "sapling-crypto" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfff8cfce16aeb38da50b8e2ed33c9018f30552beff2210c266662a021b17f38" +source = "git+https://github.com/zcash/sapling-crypto.git?rev=f228f52542749ea89f4a7cffbc0682ed9ea4b8d1#f228f52542749ea89f4a7cffbc0682ed9ea4b8d1" dependencies = [ "aes", "bellman", @@ -3684,6 +3810,7 @@ dependencies = [ "document-features", "ff", "fpe", + "getset", "group", "hex", "incrementalmerkletree", @@ -3761,6 +3888,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" dependencies = [ + "rand", "secp256k1-sys", ] @@ -4030,6 +4158,9 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spki" @@ -6221,6 +6352,7 @@ dependencies = [ "equihash", "ff", "fpe", + "getset", "group", "hex", "incrementalmerkletree", diff --git a/Cargo.toml b/Cargo.toml index ff0e81c5b4..6f0fc2932a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ bitvec = "1" blake2s_simd = "1" bls12_381 = "0.8" jubjub = "0.10" +redjubjub = "0.7" sapling = { package = "sapling-crypto", version = "0.3", default-features = false } # - Orchard @@ -66,10 +67,13 @@ orchard = { version = "0.10", default-features = false } pasta_curves = "0.5" # - Transparent -bip32 = { version = "0.5", default-features = false, features = ["secp256k1-ffi"] } +bip32 = { version = "0.5", default-features = false } ripemd = "0.1" secp256k1 = "0.27" +# Boilerplate +getset = "0.1" + # CSPRNG rand = "0.8" rand_core = "0.6" @@ -181,3 +185,7 @@ debug = true [workspace.lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(zcash_unstable, values("zfuture"))'] } + +[patch.crates-io] +orchard = { git = "https://github.com/zcash/orchard.git", rev = "bcd08e1d23e70c42a338f3e3f79d6f4c0c219805" } +sapling-crypto = { git = "https://github.com/zcash/sapling-crypto.git", rev = "f228f52542749ea89f4a7cffbc0682ed9ea4b8d1" } diff --git a/pczt/Cargo.toml b/pczt/Cargo.toml index 7570408751..4054030182 100644 --- a/pczt/Cargo.toml +++ b/pczt/Cargo.toml @@ -11,3 +11,70 @@ license.workspace = true categories.workspace = true [dependencies] +zcash_note_encryption = { workspace = true, optional = true } +zcash_primitives = { workspace = true, optional = true } +zcash_protocol = { workspace = true, optional = true } + +blake2b_simd = { workspace = true, optional = true } +rand_core = { workspace = true, optional = true } + +# Encoding +postcard = { version = "1", features = ["alloc"] } +serde.workspace = true +serde_with = "3" + +# Payment protocols +# - Transparent +secp256k1 = { workspace = true, optional = true } + +# - Sapling +bls12_381 = { workspace = true, optional = true } +ff = { workspace = true, optional = true } +jubjub = { workspace = true, optional = true } +redjubjub = { workspace = true, optional = true } +sapling = { workspace = true, optional = true } + +# - Orchard +nonempty = { workspace = true, optional = true } +orchard = { workspace = true, optional = true } +pasta_curves = { workspace = true, optional = true } + +# - Bolierplate +getset = "0.1" + +[dev-dependencies] +incrementalmerkletree.workspace = true +secp256k1 = { workspace = true, features = ["rand"] } +shardtree.workspace = true +zcash_primitives = { workspace = true, features = ["test-dependencies", "transparent-inputs"] } +zcash_proofs = { workspace = true, features = ["bundled-prover"] } +zip32.workspace = true + +[features] +orchard = [ + "dep:ff", + "dep:nonempty", + "dep:orchard", + "dep:pasta_curves", + "dep:zcash_protocol", +] +sapling = [ + "dep:bls12_381", + "dep:ff", + "dep:jubjub", + "dep:redjubjub", + "dep:sapling", + "dep:zcash_note_encryption", + "dep:zcash_protocol", +] +transparent = ["dep:secp256k1", "dep:zcash_primitives", "dep:zcash_protocol"] +zcp-builder = ["dep:zcash_primitives", "dep:zcash_protocol"] +io-finalizer = ["orchard", "sapling"] +prover = ["dep:rand_core", "sapling?/temporary-zcashd"] +signer = ["dep:blake2b_simd", "dep:rand_core", "orchard", "sapling", "transparent"] +spend-finalizer = ["transparent"] +tx-extractor = ["dep:rand_core", "orchard", "sapling", "transparent"] + +[[test]] +name = "end_to_end" +required-features = ["io-finalizer", "prover", "signer", "tx-extractor"] diff --git a/pczt/src/common.rs b/pczt/src/common.rs new file mode 100644 index 0000000000..9d6dff6ce9 --- /dev/null +++ b/pczt/src/common.rs @@ -0,0 +1,250 @@ +use std::collections::BTreeMap; + +use getset::Getters; +use serde::{Deserialize, Serialize}; + +use crate::roles::combiner::merge_map; + +pub(crate) const FLAG_TRANSPARENT_INPUTS_MODIFIABLE: u8 = 0b0000_0001; +pub(crate) const FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE: u8 = 0b0000_0010; +pub(crate) const FLAG_HAS_SIGHASH_SINGLE: u8 = 0b0000_0100; +pub(crate) const FLAG_SHIELDED_MODIFIABLE: u8 = 0b1000_0000; + +/// Global fields that are relevant to the transaction as a whole. +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Global { + // + // Transaction effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Creator when initializing the PCZT. + // + pub(crate) tx_version: u32, + pub(crate) version_group_id: u32, + + /// The consensus branch ID for the chain in which this transaction will be mined. + /// + /// Non-optional because this commits to the set of consensus rules that will apply to + /// the transaction; differences therein can affect every role. + pub(crate) consensus_branch_id: u32, + + /// The transaction locktime to use if no inputs specify a required locktime. + /// + /// - This is set by the Creator. + /// - If omitted, the fallback locktime is assumed to be 0. + pub(crate) fallback_lock_time: Option, + + pub(crate) expiry_height: u32, + + /// The [SLIP 44] coin type, indicating the network for which this transaction is + /// being constructed. + /// + /// This is technically information that could be determined indirectly from the + /// `consensus_branch_id` but is included explicitly to enable easy identification. + /// Note that this field is not included in the transaction and has no consensus + /// effect (`consensus_branch_id` fills that role). + /// + /// - This is set by the Creator. + /// - Roles that encode network-specific information (for example, derivation paths + /// for key identification) should check against this field for correctness. + /// + /// [SLIP 44]: https://github.com/satoshilabs/slips/blob/master/slip-0044.md + pub(crate) coin_type: u32, + + /// A bitfield for various transaction modification flags. + /// + /// - Bit 0 is the Transparent Inputs Modifiable Flag and indicates whether + /// transparent inputs can be modified. + /// - This is set to `true` by the Creator. + /// - This is checked by the Constructor before adding transparent inputs, and may + /// be set to `false` by the Constructor. + /// - This is set to `false` by the IO Finalizer if there are shielded spends or + /// outputs. + /// - This is set to `false` by a Signer that adds a signature that does not use + /// `SIGHASH_ANYONECANPAY` (which includes all shielded signatures). + /// - The Combiner merges this bit towards `false`. + /// - Bit 1 is the Transparent Outputs Modifiable Flag and indicates whether + /// transparent outputs can be modified. + /// - This is set to `true` by the Creator. + /// - This is checked by the Constructor before adding transparent outputs, and may + /// be set to `false` by the Constructor. + /// - This is set to `false` by the IO Finalizer if there are shielded spends or + /// outputs. + /// - This is set to `false` by a Signer that adds a signature that does not use + /// `SIGHASH_NONE` (which includes all shielded signatures). + /// - The Combiner merges this bit towards `false`. + /// - Bit 2 is the Has `SIGHASH_SINGLE` Flag and indicates whether the transaction has + /// a `SIGHASH_SINGLE` transparent signature who's input and output pairing must be + /// preserved. + /// - This is set to `false` by the Creator. + /// - This is updated by a Constructor. + /// - This is set to `true` by a Signer that adds a signature that uses + /// `SIGHASH_SINGLE`. + /// - This essentially indicates that the Constructor must iterate the transparent + /// inputs to determine whether and how to add a transparent input. + /// - The Combiner merges this bit towards `true`. + /// - Bits 3-6 must be 0. + /// - Bit 7 is the Shielded Modifiable Flag and indicates whether shielded spends or + /// outputs can be modified. + /// - This is set to `true` by the Creator. + /// - This is checked by the Constructor before adding shielded spends or outputs, + /// and may be set to `false` by the Constructor. + /// - This is set to `false` by the IO Finalizer if there are shielded spends or + /// outputs. + /// - This is set to `false` by every Signer (as all signatures commit to all + /// shielded spends and outputs). + /// - The Combiner merges this bit towards `false`. + pub(crate) tx_modifiable: u8, + + /// Proprietary fields related to the overall transaction. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +impl Global { + /// Returns whether transparent inputs can be added to or removed from the + /// transaction. + pub fn inputs_modifiable(&self) -> bool { + (self.tx_modifiable & FLAG_TRANSPARENT_INPUTS_MODIFIABLE) != 0 + } + + /// Returns whether transparent outputs can be added to or removed from the + /// transaction. + pub fn outputs_modifiable(&self) -> bool { + (self.tx_modifiable & FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE) != 0 + } + + /// Returns whether the transaction has a `SIGHASH_SINGLE` transparent signature who's + /// input and output pairing must be preserved. + pub fn has_sighash_single(&self) -> bool { + (self.tx_modifiable & FLAG_HAS_SIGHASH_SINGLE) != 0 + } + + /// Returns whether shielded spends or outputs can be added to or removed from the + /// transaction. + pub fn shielded_modifiable(&self) -> bool { + (self.tx_modifiable & FLAG_SHIELDED_MODIFIABLE) != 0 + } + + pub(crate) fn merge(mut self, other: Self) -> Option { + let Self { + tx_version, + version_group_id, + consensus_branch_id, + fallback_lock_time, + expiry_height, + coin_type, + tx_modifiable, + proprietary, + } = other; + + if self.tx_version != tx_version + || self.version_group_id != version_group_id + || self.consensus_branch_id != consensus_branch_id + || self.fallback_lock_time != fallback_lock_time + || self.expiry_height != expiry_height + || self.coin_type != coin_type + { + return None; + } + + // `tx_modifiable` is explicitly a bitmap; merge it bit-by-bit. + // - Bit 0 and Bit 1 merge towards `false`. + if (tx_modifiable & FLAG_TRANSPARENT_INPUTS_MODIFIABLE) == 0 { + self.tx_modifiable &= !FLAG_TRANSPARENT_INPUTS_MODIFIABLE; + } + if (tx_modifiable & FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE) == 0 { + self.tx_modifiable &= !FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE; + } + // - Bit 2 merges towards `true`. + if (tx_modifiable & FLAG_HAS_SIGHASH_SINGLE) != 0 { + self.tx_modifiable |= FLAG_HAS_SIGHASH_SINGLE; + } + // - Bits 3-6 must be 0. + if ((self.tx_modifiable & !FLAG_SHIELDED_MODIFIABLE) >> 3) != 0 + || ((tx_modifiable & !FLAG_SHIELDED_MODIFIABLE) >> 3) != 0 + { + return None; + } + // - Bit 7 merges towards `false`. + if (tx_modifiable & FLAG_SHIELDED_MODIFIABLE) == 0 { + self.tx_modifiable &= !FLAG_SHIELDED_MODIFIABLE; + } + + if !merge_map(&mut self.proprietary, proprietary) { + return None; + } + + Some(self) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub(crate) struct Zip32Derivation { + /// The [ZIP 32 seed fingerprint](https://zips.z.cash/zip-0032#seed-fingerprints). + pub(crate) seed_fingerprint: [u8; 32], + + /// The sequence of indices corresponding to the shielded HD path. + /// + /// Indices can be hardened or non-hardened (i.e. the hardened flag bit may be set). + /// When used with a Sapling or Orchard spend, the derivation path will generally be + /// entirely hardened; when used with a transparent spend, the derivation path will + /// generally include a non-hardened section matching either the [BIP 44] path, or the + /// path at which ephemeral addresses are derived for [ZIP 320] transactions. + /// + /// [BIP 44]: https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki + /// [ZIP 320]: https://zips.z.cash/zip-0320 + pub(crate) derivation_path: Vec, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::Global; + + #[test] + fn tx_modifiable() { + let base = Global { + tx_version: 0, + version_group_id: 0, + consensus_branch_id: 0, + fallback_lock_time: None, + expiry_height: 0, + coin_type: 0, + tx_modifiable: 0b0000_0000, + proprietary: BTreeMap::new(), + }; + + for (left, right, expected) in [ + (0b0000_0000, 0b0000_0000, Some(0b0000_0000)), + (0b0000_0000, 0b0000_0011, Some(0b0000_0000)), + (0b0000_0001, 0b0000_0011, Some(0b0000_0001)), + (0b0000_0010, 0b0000_0011, Some(0b0000_0010)), + (0b0000_0011, 0b0000_0011, Some(0b0000_0011)), + (0b0000_0000, 0b0000_0100, Some(0b0000_0100)), + (0b0000_0100, 0b0000_0100, Some(0b0000_0100)), + (0b0000_0011, 0b0000_0111, Some(0b0000_0111)), + (0b0000_0000, 0b0000_1000, None), + (0b0000_0000, 0b0001_0000, None), + (0b0000_0000, 0b0010_0000, None), + (0b0000_0000, 0b0100_0000, None), + (0b0000_0000, 0b1000_0000, Some(0b0000_0000)), + (0b1000_0000, 0b1000_0000, Some(0b1000_0000)), + ] { + let mut a = base.clone(); + a.tx_modifiable = left; + + let mut b = base.clone(); + b.tx_modifiable = right; + + assert_eq!( + a.clone() + .merge(b.clone()) + .map(|global| global.tx_modifiable), + expected + ); + assert_eq!(b.merge(a).map(|global| global.tx_modifiable), expected); + } + } +} diff --git a/pczt/src/lib.rs b/pczt/src/lib.rs index 8b13789179..d629f63949 100644 --- a/pczt/src/lib.rs +++ b/pczt/src/lib.rs @@ -1 +1,123 @@ +//! The Partially Created Zcash Transaction (PCZT) format. +//! +//! Goal is to split up the parts of creating a transaction across distinct entities. +//! The entity roles roughly match BIP 174: Partially Signed Bitcoin Transaction Format. +//! - Creator (single entity) +//! - Creates the base PCZT with no information about spends or outputs. +//! - Constructor (anyone can contribute) +//! - Adds spends and outputs to the PCZT. +//! - Before any input or output may be added, the constructor must check the +//! `Global.tx_modifiable` field. Inputs may only be added if the Inputs Modifiable +//! flag is True. Outputs may only be added if the Outputs Modifiable flag is True. +//! - A single entity is likely to be both a Creator and Constructor. +//! - IO Finalizer (anyone can execute) +//! - Sets the appropriate bits in `Global.tx_modifiable` to 0. +//! - Updates the various bsk values using the rcv information from spends and outputs. +//! - Updater (anyone can contribute) +//! - Adds information necessary for subsequent entities to proceed, such as key paths +//! for signing spends. +//! - Prover (capability holders can contribute) +//! - Needs all private information for a single spend or output. +//! - In practice, the Updater that adds a given spend or output will either act as +//! the Prover themselves, or add the necessary data, offload to the Prover, and +//! then receive back the PCZT with private data stripped and proof added. +//! - Signer (capability holders can contribute) +//! - Needs the spend authorization randomizers to create signatures. +//! - Needs sufficient information to verify that the proof is over the correct data, +//! without needing to verify the proof itself. +//! - A Signer should only need to implement: +//! - Pedersen commitments using Jubjub / Pallas arithmetic (for note and value +//! commitments) +//! - BLAKE2b and BLAKE2s (and the various PRFs / CRHs they are used in) +//! - Nullifier check (using Jubjub / Pallas arithmetic) +//! - KDF plus note decryption (AEAD_CHACHA20_POLY1305) +//! - SignatureHash algorithm +//! - Signatures (RedJubjub / RedPallas) +//! - A source of randomness. +//! - Combiner (anyone can execute) +//! - Combines several PCZTs that represent the same transaction into a single PCZT. +//! - Spend Finalizer (anyone can execute) +//! - Combines partial transparent signatures into `script_sig`s. +//! - Transaction Extractor (anyone can execute) +//! - Creates bindingSig and extracts the final transaction. +use getset::Getters; +use serde::{Deserialize, Serialize}; + +pub mod roles; + +pub mod common; +pub mod orchard; +pub mod sapling; +pub mod transparent; + +const MAGIC_BYTES: &[u8] = b"PCZT"; +const PCZT_VERSION_1: u32 = 1; + +#[cfg(feature = "zcp-builder")] +const SAPLING_TX_VERSION: u32 = 4; +const V5_TX_VERSION: u32 = 5; +const V5_VERSION_GROUP_ID: u32 = 0x26A7270A; + +/// A partially-created Zcash transaction. +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Pczt { + /// Global fields that are relevant to the transaction as a whole. + #[getset(get = "pub")] + global: common::Global, + + // + // Protocol-specific fields. + // + // Unlike the `TransactionData` type in `zcash_primitives`, these are not optional. + // This is because a PCZT does not always contain a semantically-valid transaction, + // and there may be phases where we need to store protocol-specific metadata before + // it has been determined whether there are protocol-specific inputs or outputs. + // + #[getset(get = "pub")] + transparent: transparent::Bundle, + #[getset(get = "pub")] + sapling: sapling::Bundle, + #[getset(get = "pub")] + orchard: orchard::Bundle, +} + +impl Pczt { + /// Parses a PCZT from its encoding. + pub fn parse(bytes: &[u8]) -> Result { + if bytes.len() < 8 { + return Err(ParseError::TooShort); + } + if &bytes[..4] != MAGIC_BYTES { + return Err(ParseError::NotPczt); + } + let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap()); + if version != PCZT_VERSION_1 { + return Err(ParseError::UnknownVersion(version)); + } + + // This is a v1 PCZT. + postcard::from_bytes(&bytes[8..]).map_err(ParseError::Invalid) + } + + /// Serializes this PCZT. + pub fn serialize(&self) -> Vec { + let mut bytes = vec![]; + bytes.extend_from_slice(MAGIC_BYTES); + bytes.extend_from_slice(&PCZT_VERSION_1.to_le_bytes()); + postcard::to_extend(self, bytes).expect("can serialize into memory") + } +} + +/// Errors that can occur while parsing a PCZT. +#[derive(Debug)] +pub enum ParseError { + /// The bytes do not contain a PCZT. + NotPczt, + /// The PCZT encoding was invalid. + Invalid(postcard::Error), + /// The bytes are too short to contain a PCZT. + TooShort, + /// The PCZT has an unknown version. + UnknownVersion(u32), +} diff --git a/pczt/src/orchard.rs b/pczt/src/orchard.rs new file mode 100644 index 0000000000..8d26cba951 --- /dev/null +++ b/pczt/src/orchard.rs @@ -0,0 +1,548 @@ +use std::{cmp::Ordering, collections::BTreeMap}; + +#[cfg(feature = "orchard")] +use ff::PrimeField; +use getset::Getters; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::{ + common::{Global, Zip32Derivation}, + roles::combiner::{merge_map, merge_optional}, +}; + +/// PCZT fields that are specific to producing the transaction's Orchard bundle (if any). +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Bundle { + /// The Orchard actions in this bundle. + /// + /// Entries are added by the Constructor, and modified by an Updater, IO Finalizer, + /// Signer, Combiner, or Spend Finalizer. + #[getset(get = "pub")] + pub(crate) actions: Vec, + + /// The flags for the Orchard bundle. + /// + /// Contains: + /// - `enableSpendsOrchard` flag (bit 0) + /// - `enableOutputsOrchard` flag (bit 1) + /// - Reserved, zeros (bits 2..=7) + /// + /// This is set by the Creator. The Constructor MUST only add spends and outputs that + /// are consistent with these flags (i.e. are dummies as appropriate). + pub(crate) flags: u8, + + /// The net value of Orchard spends minus outputs. + /// + /// This is initialized by the Creator, and updated by the Constructor as spends or + /// outputs are added to the PCZT. It enables per-spend and per-output values to be + /// redacted from the PCZT after they are no longer necessary. + pub(crate) value_sum: (u64, bool), + + /// The Orchard anchor for this transaction. + /// + /// Set by the Creator. + pub(crate) anchor: [u8; 32], + + /// The Orchard bundle proof. + /// + /// This is `None` until it is set by the Prover. + pub(crate) zkproof: Option>, + + /// The Orchard binding signature signing key. + /// + /// - This is `None` until it is set by the IO Finalizer. + /// - The Transaction Extractor uses this to produce the binding signature. + pub(crate) bsk: Option<[u8; 32]>, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Action { + // + // Action effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + pub(crate) cv_net: [u8; 32], + #[getset(get = "pub")] + pub(crate) spend: Spend, + #[getset(get = "pub")] + pub(crate) output: Output, + + /// The value commitment randomness. + /// + /// - This is set by the Constructor. + /// - The IO Finalizer compresses it into the bsk. + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value correctly matches `cv`. + /// + /// This opens `cv` for all participants. For Signers who don't need this information, + /// or after proofs / signatures have been applied, this can be redacted. + pub(crate) rcv: Option<[u8; 32]>, +} + +/// Information about a Sapling spend within a transaction. +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Spend { + // + // Spend-specific Action effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding a spend. + // + #[getset(get = "pub")] + pub(crate) nullifier: [u8; 32], + pub(crate) rk: [u8; 32], + + /// The spend authorization signature. + /// + /// This is set by the Signer. + #[serde_as(as = "Option<[_; 64]>")] + pub(crate) spend_auth_sig: Option<[u8; 64]>, + + /// The [raw encoding] of the Orchard payment address that received the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// + /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#orchardpaymentaddrencoding + #[serde_as(as = "Option<[_; 43]>")] + pub(crate) recipient: Option<[u8; 43]>, + + /// The value of the input being spent. + /// + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value matches `cv`, and to + /// confirm the values and change involved in the transaction. + /// + /// This exposes the input value to all participants. For Signers who don't need this + /// information, or after signatures have been applied, this can be redacted. + pub(crate) value: Option, + + /// The rho value for the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + pub(crate) rho: Option<[u8; 32]>, + + /// The seed randomness for the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + pub(crate) rseed: Option<[u8; 32]>, + + /// The full viewing key that received the note being spent. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + #[serde_as(as = "Option<[_; 96]>")] + pub(crate) fvk: Option<[u8; 96]>, + + /// A witness from the note to the bundle's anchor. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + pub(crate) witness: Option<(u32, [[u8; 32]; 32])>, + + /// The spend authorization randomizer. + /// + /// - This is chosen by the Constructor. + /// - This is required by the Signer for creating `spend_auth_sig`, and may be used to + /// validate `rk`. + /// - After `zkproof` / `spend_auth_sig` has been set, this can be redacted. + pub(crate) alpha: Option<[u8; 32]>, + + /// The ZIP 32 derivation path at which the spending key can be found for the note + /// being spent. + pub(crate) zip32_derivation: Option, + + /// The spending key for this spent note, if it is a dummy note. + /// + /// - This is chosen by the Constructor. + /// - This is required by the IO Finalizer, and is cleared by it once used. + /// - Signers MUST reject PCZTs that contain `dummy_sk` values. + pub(crate) dummy_sk: Option<[u8; 32]>, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +/// Information about an Orchard output within a transaction. +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Output { + // + // Output-specific Action effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + pub(crate) cmx: [u8; 32], + pub(crate) ephemeral_key: [u8; 32], + /// The encrypted note plaintext for the output. + /// + /// Encoded as a `Vec` because its length depends on the transaction version. + /// + /// Once we have memo bundles, we will be able to set memos independently of Outputs. + /// For now, the Constructor sets both at the same time. + pub(crate) enc_ciphertext: Vec, + /// The encrypted note plaintext for the output. + /// + /// Encoded as a `Vec` because its length depends on the transaction version. + pub(crate) out_ciphertext: Vec, + + /// The [raw encoding] of the Orchard payment address that will receive the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// + /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#orchardpaymentaddrencoding + #[serde_as(as = "Option<[_; 43]>")] + #[getset(get = "pub")] + pub(crate) recipient: Option<[u8; 43]>, + + /// The value of the output. + /// + /// This may be used by Signers to verify that the value matches `cv`, and to confirm + /// the values and change involved in the transaction. + /// + /// This exposes the value to all participants. For Signers who don't need this + /// information, we can drop the values and compress the rcvs into the bsk global. + #[getset(get = "pub")] + pub(crate) value: Option, + + /// The seed randomness for the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover, instead of disclosing `shared_secret` to them. + #[getset(get = "pub")] + pub(crate) rseed: Option<[u8; 32]>, + + /// The `ock` value used to encrypt `out_ciphertext`. + /// + /// This enables Signers to verify that `out_ciphertext` is correctly encrypted. + /// + /// This may be `None` if the Constructor added the output using an OVK policy of + /// "None", to make the output unrecoverable from the chain by the sender. + pub(crate) ock: Option<[u8; 32]>, + + /// The ZIP 32 derivation path at which the spending key can be found for the output. + pub(crate) zip32_derivation: Option, + + /// Proprietary fields related to the note being created. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +impl Bundle { + /// Merges this bundle with another. + /// + /// Returns `None` if the bundles have conflicting data. + pub(crate) fn merge( + mut self, + other: Self, + self_global: &Global, + other_global: &Global, + ) -> Option { + // Destructure `other` to ensure we handle everything. + let Self { + mut actions, + flags, + value_sum, + anchor, + zkproof, + bsk, + } = other; + + if self.flags != flags { + return None; + } + + // If `bsk` is set on either bundle, the IO Finalizer has run, which means we + // cannot have differing numbers of actions, and the value sums must match. + match (self.bsk.as_mut(), bsk) { + (Some(lhs), Some(rhs)) if lhs != &rhs => return None, + (Some(_), _) | (_, Some(_)) + if self.actions.len() != actions.len() || self.value_sum != value_sum => + { + return None + } + // IO Finalizer has run, and neither bundle has excess spends or outputs. + (Some(_), _) | (_, Some(_)) => (), + // IO Finalizer has not run on either bundle. + (None, None) => match ( + self_global.shielded_modifiable(), + other_global.shielded_modifiable(), + self.actions.len().cmp(&actions.len()), + ) { + // Fail if the merge would add actions to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more actions than us, move them over; these + // cannot conflict by construction. + (true, _, Ordering::Less) => { + self.actions.extend(actions.drain(self.actions.len()..)); + + // We check below that the overlapping actions match. Assuming here + // that they will, we can take the other bundle's value sum. + self.value_sum = value_sum; + } + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + }, + } + + if self.anchor != anchor { + return None; + } + + if !merge_optional(&mut self.zkproof, zkproof) { + return None; + } + + // Leverage the early-exit behaviour of zip to confirm that the remaining data in + // the other bundle matches this one. + for (lhs, rhs) in self.actions.iter_mut().zip(actions.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Action { + cv_net, + spend: + Spend { + nullifier, + rk, + spend_auth_sig, + recipient, + value, + rho, + rseed, + fvk, + witness, + alpha, + zip32_derivation: spend_zip32_derivation, + dummy_sk, + proprietary: spend_proprietary, + }, + output: + Output { + cmx, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + recipient: output_recipient, + value: output_value, + rseed: output_rseed, + ock, + zip32_derivation: output_zip32_derivation, + proprietary: output_proprietary, + }, + rcv, + } = rhs; + + if lhs.cv_net != cv_net + || lhs.spend.nullifier != nullifier + || lhs.spend.rk != rk + || lhs.output.cmx != cmx + || lhs.output.ephemeral_key != ephemeral_key + || lhs.output.enc_ciphertext != enc_ciphertext + || lhs.output.out_ciphertext != out_ciphertext + { + return None; + } + + if !(merge_optional(&mut lhs.spend.spend_auth_sig, spend_auth_sig) + && merge_optional(&mut lhs.spend.recipient, recipient) + && merge_optional(&mut lhs.spend.value, value) + && merge_optional(&mut lhs.spend.rho, rho) + && merge_optional(&mut lhs.spend.rseed, rseed) + && merge_optional(&mut lhs.spend.fvk, fvk) + && merge_optional(&mut lhs.spend.witness, witness) + && merge_optional(&mut lhs.spend.alpha, alpha) + && merge_optional(&mut lhs.spend.zip32_derivation, spend_zip32_derivation) + && merge_optional(&mut lhs.spend.dummy_sk, dummy_sk) + && merge_map(&mut lhs.spend.proprietary, spend_proprietary) + && merge_optional(&mut lhs.output.recipient, output_recipient) + && merge_optional(&mut lhs.output.value, output_value) + && merge_optional(&mut lhs.output.rseed, output_rseed) + && merge_optional(&mut lhs.output.ock, ock) + && merge_optional(&mut lhs.output.zip32_derivation, output_zip32_derivation) + && merge_map(&mut lhs.output.proprietary, output_proprietary) + && merge_optional(&mut lhs.rcv, rcv)) + { + return None; + } + } + + Some(self) + } +} + +#[cfg(feature = "orchard")] +impl Bundle { + pub(crate) fn into_parsed(self) -> Result { + let actions = self + .actions + .into_iter() + .map(|action| { + let spend = orchard::pczt::Spend::parse( + action.spend.nullifier, + action.spend.rk, + action.spend.spend_auth_sig, + action.spend.recipient, + action.spend.value, + action.spend.rho, + action.spend.rseed, + action.spend.fvk, + action.spend.witness, + action.spend.alpha, + action + .spend + .zip32_derivation + .map(|z| { + orchard::pczt::Zip32Derivation::parse( + z.seed_fingerprint, + z.derivation_path, + ) + }) + .transpose()?, + action.spend.dummy_sk, + action.spend.proprietary, + )?; + + let output = orchard::pczt::Output::parse( + *spend.nullifier(), + action.output.cmx, + action.output.ephemeral_key, + action.output.enc_ciphertext, + action.output.out_ciphertext, + action.output.recipient, + action.output.value, + action.output.rseed, + action.output.ock, + action + .output + .zip32_derivation + .map(|z| { + orchard::pczt::Zip32Derivation::parse( + z.seed_fingerprint, + z.derivation_path, + ) + }) + .transpose()?, + action.output.proprietary, + )?; + + orchard::pczt::Action::parse(action.cv_net, spend, output, action.rcv) + }) + .collect::>()?; + + orchard::pczt::Bundle::parse( + actions, + self.flags, + self.value_sum, + self.anchor, + self.zkproof, + self.bsk, + ) + } + + pub(crate) fn serialize_from(bundle: orchard::pczt::Bundle) -> Self { + let actions = bundle + .actions() + .iter() + .map(|action| { + let spend = action.spend(); + let output = action.output(); + + Action { + cv_net: action.cv_net().to_bytes(), + spend: Spend { + nullifier: spend.nullifier().to_bytes(), + rk: spend.rk().into(), + spend_auth_sig: spend.spend_auth_sig().as_ref().map(|s| s.into()), + recipient: action + .spend() + .recipient() + .map(|recipient| recipient.to_raw_address_bytes()), + value: spend.value().map(|value| value.inner()), + rho: spend.rho().map(|rho| rho.to_bytes()), + rseed: spend.rseed().map(|rseed| *rseed.as_bytes()), + fvk: spend.fvk().as_ref().map(|fvk| fvk.to_bytes()), + witness: spend.witness().as_ref().map(|witness| { + ( + u32::try_from(u64::from(witness.position())) + .expect("Sapling positions fit in u32"), + witness + .auth_path() + .iter() + .map(|node| node.to_bytes()) + .collect::>()[..] + .try_into() + .expect("path is length 32"), + ) + }), + alpha: spend.alpha().map(|alpha| alpha.to_repr()), + zip32_derivation: spend.zip32_derivation().as_ref().map(|z| { + Zip32Derivation { + seed_fingerprint: *z.seed_fingerprint(), + derivation_path: z + .derivation_path() + .iter() + .map(|i| i.index()) + .collect(), + } + }), + dummy_sk: action + .spend() + .dummy_sk() + .map(|dummy_sk| *dummy_sk.to_bytes()), + proprietary: spend.proprietary().clone(), + }, + output: Output { + cmx: output.cmx().to_bytes(), + ephemeral_key: output.encrypted_note().epk_bytes, + enc_ciphertext: output.encrypted_note().enc_ciphertext.to_vec(), + out_ciphertext: output.encrypted_note().out_ciphertext.to_vec(), + recipient: action + .output() + .recipient() + .map(|recipient| recipient.to_raw_address_bytes()), + value: output.value().map(|value| value.inner()), + rseed: output.rseed().map(|rseed| *rseed.as_bytes()), + ock: output.ock().as_ref().map(|ock| ock.0), + zip32_derivation: output.zip32_derivation().as_ref().map(|z| { + Zip32Derivation { + seed_fingerprint: *z.seed_fingerprint(), + derivation_path: z + .derivation_path() + .iter() + .map(|i| i.index()) + .collect(), + } + }), + proprietary: output.proprietary().clone(), + }, + rcv: action.rcv().as_ref().map(|rcv| rcv.to_bytes()), + } + }) + .collect(); + + let value_sum = { + let (magnitude, sign) = bundle.value_sum().magnitude_sign(); + (magnitude, matches!(sign, orchard::value::Sign::Negative)) + }; + + Self { + actions, + flags: bundle.flags().to_byte(), + value_sum, + anchor: bundle.anchor().to_bytes(), + zkproof: bundle + .zkproof() + .as_ref() + .map(|zkproof| zkproof.as_ref().to_vec()), + bsk: bundle.bsk().as_ref().map(|bsk| bsk.into()), + } + } +} diff --git a/pczt/src/roles.rs b/pczt/src/roles.rs new file mode 100644 index 0000000000..711da8f7b4 --- /dev/null +++ b/pczt/src/roles.rs @@ -0,0 +1,64 @@ +pub mod creator; + +#[cfg(feature = "io-finalizer")] +pub mod io_finalizer; + +pub mod updater; + +#[cfg(feature = "prover")] +pub mod prover; + +#[cfg(feature = "signer")] +pub mod signer; + +pub mod combiner; + +#[cfg(feature = "spend-finalizer")] +pub mod spend_finalizer; + +#[cfg(feature = "tx-extractor")] +pub mod tx_extractor; + +#[cfg(test)] +mod tests { + #[cfg(feature = "tx-extractor")] + #[test] + fn extract_fails_on_empty() { + use zcash_protocol::consensus::BranchId; + + use crate::roles::{ + creator::Creator, + tx_extractor::{self, TransactionExtractor}, + }; + + let pczt = Creator::new(BranchId::Nu6.into(), 10_000_000, 133, [0; 32], [0; 32]).build(); + + // Extraction fails because we haven't run the IO Finalizer. + // Extraction fails in Sapling because we happen to extract it before Orchard. + assert!(matches!( + TransactionExtractor::new(pczt).extract().unwrap_err(), + tx_extractor::Error::Sapling(tx_extractor::SaplingError::Extract( + sapling::pczt::TxExtractorError::MissingBindingSignatureSigningKey + )), + )); + } + + #[cfg(feature = "io-finalizer")] + #[test] + fn io_finalizer_fails_on_empty() { + use zcash_protocol::consensus::BranchId; + + use crate::roles::{ + creator::Creator, + io_finalizer::{self, IoFinalizer}, + }; + + let pczt = Creator::new(BranchId::Nu6.into(), 10_000_000, 133, [0; 32], [0; 32]).build(); + + // IO finalization fails on spends because we happen to check them first. + assert!(matches!( + IoFinalizer::new(pczt).finalize_io().unwrap_err(), + io_finalizer::Error::NoSpends, + )); + } +} diff --git a/pczt/src/roles/combiner/mod.rs b/pczt/src/roles/combiner/mod.rs new file mode 100644 index 0000000000..e41ad5ce85 --- /dev/null +++ b/pczt/src/roles/combiner/mod.rs @@ -0,0 +1,102 @@ +use std::collections::BTreeMap; + +use crate::Pczt; + +pub struct Combiner { + pczts: Vec, +} + +impl Combiner { + /// Instantiates the Combiner role with the given PCZTs. + pub fn new(pczts: Vec) -> Self { + Self { pczts } + } + + /// Combines the PCZTs. + pub fn combine(self) -> Result { + self.pczts + .into_iter() + .try_fold(None, |acc, pczt| match acc { + None => Ok(Some(pczt)), + Some(acc) => merge(acc, pczt).map(Some), + }) + .transpose() + .unwrap_or(Err(Error::NoPczts)) + } +} + +fn merge(lhs: Pczt, rhs: Pczt) -> Result { + // Per-protocol bundles are merged first, because each is only interpretable in the + // context of its own global. + let transparent = lhs + .transparent + .merge(rhs.transparent, &lhs.global, &rhs.global) + .ok_or(Error::DataMismatch)?; + let sapling = lhs + .sapling + .merge(rhs.sapling, &lhs.global, &rhs.global) + .ok_or(Error::DataMismatch)?; + let orchard = lhs + .orchard + .merge(rhs.orchard, &lhs.global, &rhs.global) + .ok_or(Error::DataMismatch)?; + + // Now that the per-protocol bundles are merged, merge the globals. + let global = lhs.global.merge(rhs.global).ok_or(Error::DataMismatch)?; + + Ok(Pczt { + global, + transparent, + sapling, + orchard, + }) +} + +/// Merges two values for an optional field together. +/// +/// Returns `false` if the values cannot be merged. +pub(crate) fn merge_optional(lhs: &mut Option, rhs: Option) -> bool { + match (&lhs, rhs) { + // If the RHS is not present, keep the LHS. + (_, None) => (), + // If the LHS is not present, set it to the RHS. + (None, Some(rhs)) => *lhs = Some(rhs), + // If both are present and are equal, nothing to do. + (Some(lhs), Some(rhs)) if lhs == &rhs => (), + // If both are present and are not equal, fail. Here we differ from BIP 174. + (Some(_), Some(_)) => return false, + } + + // Success! + true +} + +/// Merges two maps together. +/// +/// Returns `false` if the values cannot be merged. +pub(crate) fn merge_map( + lhs: &mut BTreeMap, + rhs: BTreeMap, +) -> bool { + for (key, rhs_value) in rhs.into_iter() { + if let Some(lhs_value) = lhs.get_mut(&key) { + // If the key is present in both maps, and their values are not equal, fail. + // Here we differ from BIP 174. + if lhs_value != &rhs_value { + return false; + } + } else { + lhs.insert(key, rhs_value); + } + } + + // Success! + true +} + +/// Errors that can occur while combining PCZTs. +#[derive(Debug)] +pub enum Error { + NoPczts, + DataMismatch, +} diff --git a/pczt/src/roles/creator/mod.rs b/pczt/src/roles/creator/mod.rs new file mode 100644 index 0000000000..35e131dc76 --- /dev/null +++ b/pczt/src/roles/creator/mod.rs @@ -0,0 +1,170 @@ +use std::collections::BTreeMap; + +use crate::{ + common::{ + FLAG_SHIELDED_MODIFIABLE, FLAG_TRANSPARENT_INPUTS_MODIFIABLE, + FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE, + }, + Pczt, V5_TX_VERSION, V5_VERSION_GROUP_ID, +}; + +/// Initial flags allowing any modification. +const INITIAL_TX_MODIFIABLE: u8 = FLAG_TRANSPARENT_INPUTS_MODIFIABLE + | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE + | FLAG_SHIELDED_MODIFIABLE; + +const ORCHARD_SPENDS_AND_OUTPUTS_ENABLED: u8 = 0b0000_0011; + +pub struct Creator { + tx_version: u32, + version_group_id: u32, + consensus_branch_id: u32, + fallback_lock_time: Option, + expiry_height: u32, + coin_type: u32, + orchard_flags: u8, + sapling_anchor: [u8; 32], + orchard_anchor: [u8; 32], +} + +impl Creator { + pub fn new( + consensus_branch_id: u32, + expiry_height: u32, + coin_type: u32, + sapling_anchor: [u8; 32], + orchard_anchor: [u8; 32], + ) -> Self { + Self { + // Default to v5 transaction format. + tx_version: V5_TX_VERSION, + version_group_id: V5_VERSION_GROUP_ID, + consensus_branch_id, + fallback_lock_time: None, + expiry_height, + coin_type, + orchard_flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED, + sapling_anchor, + orchard_anchor, + } + } + + pub fn with_fallback_lock_time(mut self, fallback: u32) -> Self { + self.fallback_lock_time = Some(fallback); + self + } + + #[cfg(feature = "orchard")] + pub fn with_orchard_flags(mut self, orchard_flags: orchard::bundle::Flags) -> Self { + self.orchard_flags = orchard_flags.to_byte(); + self + } + + pub fn build(self) -> Pczt { + Pczt { + global: crate::common::Global { + tx_version: self.tx_version, + version_group_id: self.version_group_id, + consensus_branch_id: self.consensus_branch_id, + fallback_lock_time: self.fallback_lock_time, + expiry_height: self.expiry_height, + coin_type: self.coin_type, + tx_modifiable: INITIAL_TX_MODIFIABLE, + proprietary: BTreeMap::new(), + }, + transparent: crate::transparent::Bundle { + inputs: vec![], + outputs: vec![], + }, + sapling: crate::sapling::Bundle { + spends: vec![], + outputs: vec![], + value_sum: 0, + anchor: self.sapling_anchor, + bsk: None, + }, + orchard: crate::orchard::Bundle { + actions: vec![], + flags: self.orchard_flags, + value_sum: (0, true), + anchor: self.orchard_anchor, + zkproof: None, + bsk: None, + }, + } + } + + /// Builds a PCZT from the output of a [`Builder`]. + /// + /// Returns `None` if the `TxVersion` is incompatible with PCZTs. + /// + /// [`Builder`]: zcash_primitives::transaction::builder::Builder + #[cfg(feature = "zcp-builder")] + pub fn build_from_parts( + parts: zcash_primitives::transaction::builder::PcztParts

, + ) -> Option { + use zcash_primitives::transaction::sighash::{SIGHASH_ANYONECANPAY, SIGHASH_SINGLE}; + use zcash_protocol::consensus::NetworkConstants; + + use crate::{common::FLAG_HAS_SIGHASH_SINGLE, SAPLING_TX_VERSION}; + + let tx_version = match parts.version { + zcash_primitives::transaction::TxVersion::Sprout(_) + | zcash_primitives::transaction::TxVersion::Overwinter => None, + zcash_primitives::transaction::TxVersion::Sapling => Some(SAPLING_TX_VERSION), + zcash_primitives::transaction::TxVersion::Zip225 => Some(V5_TX_VERSION), + }?; + + // Spends and outputs not modifiable. + let mut tx_modifiable = 0b0000_0000; + // Check if any input is using `SIGHASH_SINGLE` (with or without `ANYONECANPAY`). + if parts.transparent.as_ref().map_or(false, |bundle| { + bundle.inputs().iter().any(|input| { + (input.sighash_type().encode() & !SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE + }) + }) { + tx_modifiable |= FLAG_HAS_SIGHASH_SINGLE; + } + + Some(Pczt { + global: crate::common::Global { + tx_version, + version_group_id: parts.version.version_group_id(), + consensus_branch_id: parts.consensus_branch_id.into(), + fallback_lock_time: Some(parts.lock_time), + expiry_height: parts.expiry_height.into(), + coin_type: parts.params.network_type().coin_type(), + tx_modifiable, + proprietary: BTreeMap::new(), + }, + transparent: parts + .transparent + .map(crate::transparent::Bundle::serialize_from) + .unwrap_or_else(|| crate::transparent::Bundle { + inputs: vec![], + outputs: vec![], + }), + sapling: parts + .sapling + .map(crate::sapling::Bundle::serialize_from) + .unwrap_or_else(|| crate::sapling::Bundle { + spends: vec![], + outputs: vec![], + value_sum: 0, + anchor: sapling::Anchor::empty_tree().to_bytes(), + bsk: None, + }), + orchard: parts + .orchard + .map(crate::orchard::Bundle::serialize_from) + .unwrap_or_else(|| crate::orchard::Bundle { + actions: vec![], + flags: ORCHARD_SPENDS_AND_OUTPUTS_ENABLED, + value_sum: (0, true), + anchor: orchard::Anchor::empty_tree().to_bytes(), + zkproof: None, + bsk: None, + }), + }) + } +} diff --git a/pczt/src/roles/io_finalizer/mod.rs b/pczt/src/roles/io_finalizer/mod.rs new file mode 100644 index 0000000000..d013292349 --- /dev/null +++ b/pczt/src/roles/io_finalizer/mod.rs @@ -0,0 +1,121 @@ +use rand_core::OsRng; +use zcash_primitives::transaction::{ + components::transparent, sighash::SignableInput, sighash_v5::v5_signature_hash, + txid::TxIdDigester, +}; + +use crate::{ + common::{ + FLAG_SHIELDED_MODIFIABLE, FLAG_TRANSPARENT_INPUTS_MODIFIABLE, + FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE, + }, + Pczt, V5_TX_VERSION, V5_VERSION_GROUP_ID, +}; + +use super::signer::pczt_to_tx_data; + +pub struct IoFinalizer { + pczt: Pczt, +} + +impl IoFinalizer { + /// Instantiates the IO Finalizer role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Finalizes the IO of the PCZT. + pub fn finalize_io(self) -> Result { + let Self { pczt } = self; + + let has_shielded_spends = + !(pczt.sapling.spends.is_empty() && pczt.orchard.actions.is_empty()); + let has_shielded_outputs = + !(pczt.sapling.outputs.is_empty() && pczt.orchard.actions.is_empty()); + + // We can't build a transaction that has no spends or outputs. + // However, we don't attempt to reject an entirely dummy transaction. + if pczt.transparent.inputs.is_empty() && !has_shielded_spends { + return Err(Error::NoSpends); + } + if pczt.transparent.outputs.is_empty() && !has_shielded_outputs { + return Err(Error::NoOutputs); + } + + let Pczt { + mut global, + transparent, + sapling, + orchard, + } = pczt; + + // After shielded IO finalization, the transaction effects cannot be modified + // because dummy spends will have been signed. + if has_shielded_spends || has_shielded_outputs { + global.tx_modifiable &= !(FLAG_TRANSPARENT_INPUTS_MODIFIABLE + | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE + | FLAG_SHIELDED_MODIFIABLE); + } + + let transparent = transparent.into_parsed().map_err(Error::TransparentParse)?; + let mut sapling = sapling.into_parsed().map_err(Error::SaplingParse)?; + let mut orchard = orchard.into_parsed().map_err(Error::OrchardParse)?; + + let tx_data = pczt_to_tx_data(&global, &transparent, &sapling, &orchard)?; + let txid_parts = tx_data.digest(TxIdDigester); + + // TODO: Pick sighash based on tx version. + match (global.tx_version, global.version_group_id) { + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(()), + (version, version_group_id) => Err(Error::UnsupportedTxVersion { + version, + version_group_id, + }), + }?; + let shielded_sighash = v5_signature_hash(&tx_data, &SignableInput::Shielded, &txid_parts) + .as_ref() + .try_into() + .expect("correct length"); + + sapling + .finalize_io(shielded_sighash, OsRng) + .map_err(Error::SaplingFinalize)?; + orchard + .finalize_io(shielded_sighash, OsRng) + .map_err(Error::OrchardFinalize)?; + + Ok(Pczt { + global, + transparent: crate::transparent::Bundle::serialize_from(transparent), + sapling: crate::sapling::Bundle::serialize_from(sapling), + orchard: crate::orchard::Bundle::serialize_from(orchard), + }) + } +} + +/// Errors that can occur while finalizing the IO of a PCZT. +#[derive(Debug)] +pub enum Error { + NoOutputs, + NoSpends, + OrchardFinalize(orchard::pczt::IoFinalizerError), + OrchardParse(orchard::pczt::ParseError), + SaplingFinalize(sapling::pczt::IoFinalizerError), + SaplingParse(sapling::pczt::ParseError), + Sign(super::signer::Error), + TransparentParse(transparent::pczt::ParseError), + UnsupportedTxVersion { version: u32, version_group_id: u32 }, +} + +impl From for Error { + fn from(e: super::signer::Error) -> Self { + match e { + super::signer::Error::OrchardParse(parse_error) => Error::OrchardParse(parse_error), + super::signer::Error::SaplingParse(parse_error) => Error::SaplingParse(parse_error), + super::signer::Error::TransparentParse(parse_error) => { + Error::TransparentParse(parse_error) + } + _ => Error::Sign(e), + } + } +} diff --git a/pczt/src/roles/prover/mod.rs b/pczt/src/roles/prover/mod.rs new file mode 100644 index 0000000000..9772a93890 --- /dev/null +++ b/pczt/src/roles/prover/mod.rs @@ -0,0 +1,27 @@ +use crate::Pczt; + +#[cfg(feature = "orchard")] +mod orchard; +#[cfg(feature = "orchard")] +pub use orchard::OrchardError; + +#[cfg(feature = "sapling")] +mod sapling; +#[cfg(feature = "sapling")] +pub use sapling::SaplingError; + +pub struct Prover { + pczt: Pczt, +} + +impl Prover { + /// Instantiates the Prover role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Finishes the Prover role, returning the updated PCZT. + pub fn finish(self) -> Pczt { + self.pczt + } +} diff --git a/pczt/src/roles/prover/orchard.rs b/pczt/src/roles/prover/orchard.rs new file mode 100644 index 0000000000..237b9bdc3e --- /dev/null +++ b/pczt/src/roles/prover/orchard.rs @@ -0,0 +1,37 @@ +use orchard::circuit::ProvingKey; +use rand_core::OsRng; + +use crate::Pczt; + +impl super::Prover { + pub fn create_orchard_proof(self, pk: &ProvingKey) -> Result { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = orchard.into_parsed().map_err(OrchardError::Parser)?; + + bundle + .create_proof(pk, OsRng) + .map_err(OrchardError::Prover)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling, + orchard: crate::orchard::Bundle::serialize_from(bundle), + }, + }) + } +} + +/// Errors that can occur while creating Orchard proofs for a PCZT. +#[derive(Debug)] +pub enum OrchardError { + Parser(orchard::pczt::ParseError), + Prover(orchard::pczt::ProverError), +} diff --git a/pczt/src/roles/prover/sapling.rs b/pczt/src/roles/prover/sapling.rs new file mode 100644 index 0000000000..877e22bab7 --- /dev/null +++ b/pczt/src/roles/prover/sapling.rs @@ -0,0 +1,45 @@ +use rand_core::OsRng; +use sapling::prover::{OutputProver, SpendProver}; + +use crate::Pczt; + +impl super::Prover { + pub fn create_sapling_proofs( + self, + spend_prover: &S, + output_prover: &O, + ) -> Result + where + S: SpendProver, + O: OutputProver, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = sapling.into_parsed().map_err(SaplingError::Parser)?; + + bundle + .create_proofs(spend_prover, output_prover, OsRng) + .map_err(SaplingError::Prover)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling: crate::sapling::Bundle::serialize_from(bundle), + orchard, + }, + }) + } +} + +/// Errors that can occur while creating Sapling proofs for a PCZT. +#[derive(Debug)] +pub enum SaplingError { + Parser(sapling::pczt::ParseError), + Prover(sapling::pczt::ProverError), +} diff --git a/pczt/src/roles/signer/mod.rs b/pczt/src/roles/signer/mod.rs new file mode 100644 index 0000000000..bdb74db9bd --- /dev/null +++ b/pczt/src/roles/signer/mod.rs @@ -0,0 +1,338 @@ +use blake2b_simd::Hash as Blake2bHash; +use rand_core::OsRng; +use zcash_primitives::transaction::{ + components::transparent, + sighash::{SignableInput, SIGHASH_ANYONECANPAY, SIGHASH_NONE, SIGHASH_SINGLE}, + sighash_v5::v5_signature_hash, + txid::TxIdDigester, + Authorization, TransactionData, TxDigests, TxVersion, +}; +use zcash_protocol::consensus::BranchId; + +use crate::{ + common::{ + Global, FLAG_HAS_SIGHASH_SINGLE, FLAG_SHIELDED_MODIFIABLE, + FLAG_TRANSPARENT_INPUTS_MODIFIABLE, FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE, + }, + Pczt, +}; + +use super::tx_extractor::determine_lock_time; + +const V5_TX_VERSION: u32 = 5; +const V5_VERSION_GROUP_ID: u32 = 0x26A7270A; + +pub struct Signer { + global: Global, + transparent: transparent::pczt::Bundle, + sapling: sapling::pczt::Bundle, + orchard: orchard::pczt::Bundle, + /// Cached across multiple signatures. + tx_data: TransactionData, + txid_parts: TxDigests, + shielded_sighash: [u8; 32], + secp: secp256k1::Secp256k1, +} + +impl Signer { + /// Instantiates the Signer role with the given PCZT. + pub fn new(pczt: Pczt) -> Result { + let Pczt { + global, + transparent, + sapling, + orchard, + } = pczt; + + let transparent = transparent.into_parsed().map_err(Error::TransparentParse)?; + let sapling = sapling.into_parsed().map_err(Error::SaplingParse)?; + let orchard = orchard.into_parsed().map_err(Error::OrchardParse)?; + + let tx_data = pczt_to_tx_data(&global, &transparent, &sapling, &orchard)?; + let txid_parts = tx_data.digest(TxIdDigester); + + // TODO: Pick sighash based on tx version. + match (global.tx_version, global.version_group_id) { + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(()), + (version, version_group_id) => Err(Error::Global(GlobalError::UnsupportedTxVersion { + version, + version_group_id, + })), + }?; + let shielded_sighash = v5_signature_hash(&tx_data, &SignableInput::Shielded, &txid_parts) + .as_ref() + .try_into() + .expect("correct length"); + + Ok(Self { + global, + transparent, + sapling, + orchard, + tx_data, + txid_parts, + shielded_sighash, + secp: secp256k1::Secp256k1::signing_only(), + }) + } + + /// Signs the transparent spend at the given index with the given spending key. + /// + /// It is the caller's responsibility to perform any semantic validity checks on the + /// PCZT (for example, comfirming that the change amounts are correct) before calling + /// this method. + pub fn sign_transparent( + &mut self, + index: usize, + sk: &secp256k1::SecretKey, + ) -> Result<(), Error> { + let input = self + .transparent + .inputs_mut() + .get_mut(index) + .ok_or(Error::InvalidIndex)?; + + // Check consistency of the input being signed. + // TODO + + input + .sign(index, &self.tx_data, &self.txid_parts, sk, &self.secp) + .map_err(Error::TransparentSign)?; + + // Update transaction modifiability: + // - If the Signer added a signature that does not use `SIGHASH_ANYONECANPAY`, the + // Transparent Inputs Modifiable Flag must be set to False (because the + // signature commits to all inputs, not just the one at `index`). + if input.sighash_type().encode() & SIGHASH_ANYONECANPAY == 0 { + self.global.tx_modifiable &= !FLAG_TRANSPARENT_INPUTS_MODIFIABLE; + } + // - If the Signer added a signature that does not use `SIGHASH_NONE`, the + // Transparent Outputs Modifiable Flag must be set to False. Note that this + // applies to `SIGHASH_SINGLE` because we could otherwise remove the output at + // `index`, which would not remove the signature. + if (input.sighash_type().encode() & !SIGHASH_ANYONECANPAY) != SIGHASH_NONE { + self.global.tx_modifiable &= !FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE; + } + // - If the Signer added a signature that uses `SIGHASH_SINGLE`, the Has + // `SIGHASH_SINGLE` flag must be set to True. + if (input.sighash_type().encode() & !SIGHASH_ANYONECANPAY) == SIGHASH_SINGLE { + self.global.tx_modifiable |= FLAG_HAS_SIGHASH_SINGLE; + } + // - Always set the Shielded Modifiable Flag to False. + self.global.tx_modifiable &= !FLAG_SHIELDED_MODIFIABLE; + + Ok(()) + } + + /// Signs the Sapling spend at the given index with the given spend authorizing key. + /// + /// It is the caller's responsibility to perform any semantic validity checks on the + /// PCZT (for example, comfirming that the change amounts are correct) before calling + /// this method. + pub fn sign_sapling( + &mut self, + index: usize, + ask: &sapling::keys::SpendAuthorizingKey, + ) -> Result<(), Error> { + let spend = self + .sapling + .spends_mut() + .get_mut(index) + .ok_or(Error::InvalidIndex)?; + + // Check consistency of the input being signed. + let note_from_fields = spend + .recipient() + .zip(spend.value().as_ref()) + .zip(spend.rseed().as_ref()) + .map(|((recipient, value), rseed)| { + sapling::Note::from_parts(recipient, *value, *rseed) + }); + + if let Some(note) = note_from_fields { + let tx_spend = self + .tx_data + .sapling_bundle() + .expect("index checked above") + .shielded_spends() + .get(index) + .expect("index checked above"); + + let proof_generation_key = spend + .proof_generation_key() + .as_ref() + .ok_or(Error::MissingProofGenerationKey)?; + + let nk = proof_generation_key.to_viewing_key().nk; + + let merkle_path = spend.witness().as_ref().ok_or(Error::MissingWitness)?; + + if ¬e.nf(&nk, merkle_path.position().into()) != tx_spend.nullifier() { + return Err(Error::InvalidNullifier); + } + } + + spend + .sign(self.shielded_sighash, ask, OsRng) + .map_err(Error::SaplingSign)?; + + // Update transaction modifiability: all transaction effects have been committed + // to by the signature. + self.global.tx_modifiable &= !(FLAG_TRANSPARENT_INPUTS_MODIFIABLE + | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE + | FLAG_SHIELDED_MODIFIABLE); + + Ok(()) + } + + /// Signs the Orchard spend at the given index with the given spend authorizing key. + /// + /// It is the caller's responsibility to perform any semantic validity checks on the + /// PCZT (for example, comfirming that the change amounts are correct) before calling + /// this method. + pub fn sign_orchard( + &mut self, + index: usize, + ask: &orchard::keys::SpendAuthorizingKey, + ) -> Result<(), Error> { + let action = self + .orchard + .actions_mut() + .get_mut(index) + .ok_or(Error::InvalidIndex)?; + + // Check consistency of the input being signed. + let note_from_fields = action + .spend() + .recipient() + .zip(action.spend().value().as_ref()) + .zip(action.spend().rho().as_ref()) + .zip(action.spend().rseed().as_ref()) + .map(|(((recipient, value), rho), rseed)| { + orchard::Note::from_parts(recipient, *value, *rho, *rseed) + .into_option() + .ok_or(Error::InvalidNote) + }) + .transpose()?; + + if let Some(note) = note_from_fields { + let tx_action = self + .tx_data + .orchard_bundle() + .expect("index checked above") + .actions() + .get(index) + .expect("index checked above"); + + let fvk = action + .spend() + .fvk() + .as_ref() + .ok_or(Error::MissingFullViewingKey)?; + + if ¬e.nullifier(fvk) != tx_action.nullifier() { + return Err(Error::InvalidNullifier); + } + } + + action + .sign(self.shielded_sighash, ask, OsRng) + .map_err(Error::OrchardSign)?; + + // Update transaction modifiability: all transaction effects have been committed + // to by the signature. + self.global.tx_modifiable &= !(FLAG_TRANSPARENT_INPUTS_MODIFIABLE + | FLAG_TRANSPARENT_OUTPUTS_MODIFIABLE + | FLAG_SHIELDED_MODIFIABLE); + + Ok(()) + } + + /// Finishes the Signer role, returning the updated PCZT. + pub fn finish(self) -> Pczt { + Pczt { + global: self.global, + transparent: crate::transparent::Bundle::serialize_from(self.transparent), + sapling: crate::sapling::Bundle::serialize_from(self.sapling), + orchard: crate::orchard::Bundle::serialize_from(self.orchard), + } + } +} + +/// Extracts an unauthorized `TransactionData` from the PCZT. +/// +/// We don't care about existing proofs or signatures here, because they do not affect the +/// sighash; we only want the effects of the transaction. +pub(crate) fn pczt_to_tx_data( + global: &Global, + transparent: &transparent::pczt::Bundle, + sapling: &sapling::pczt::Bundle, + orchard: &orchard::pczt::Bundle, +) -> Result, Error> { + let version = match (global.tx_version, global.version_group_id) { + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::Zip225), + (version, version_group_id) => Err(Error::Global(GlobalError::UnsupportedTxVersion { + version, + version_group_id, + })), + }?; + + let consensus_branch_id = BranchId::try_from(global.consensus_branch_id) + .map_err(|_| Error::Global(GlobalError::UnknownConsensusBranchId))?; + + let transparent_bundle = transparent + .extract_effects() + .map_err(Error::TransparentExtract)?; + + let sapling_bundle = sapling.extract_effects().map_err(Error::SaplingExtract)?; + + let orchard_bundle = orchard.extract_effects().map_err(Error::OrchardExtract)?; + + Ok(TransactionData::from_parts( + version, + consensus_branch_id, + determine_lock_time(global, transparent.inputs()) + .map_err(|_| Error::IncompatibleLockTimes)?, + global.expiry_height.into(), + transparent_bundle, + None, + sapling_bundle, + orchard_bundle, + )) +} + +pub(crate) struct EffectsOnly; + +impl Authorization for EffectsOnly { + type TransparentAuth = transparent::EffectsOnly; + type SaplingAuth = sapling::bundle::EffectsOnly; + type OrchardAuth = orchard::bundle::EffectsOnly; +} + +/// Errors that can occur while creating signatures for a PCZT. +#[derive(Debug)] +pub enum Error { + Global(GlobalError), + IncompatibleLockTimes, + InvalidIndex, + InvalidNote, + InvalidNullifier, + MissingFullViewingKey, + MissingProofGenerationKey, + MissingWitness, + OrchardExtract(orchard::pczt::TxExtractorError), + OrchardParse(orchard::pczt::ParseError), + OrchardSign(orchard::pczt::SignerError), + SaplingExtract(sapling::pczt::TxExtractorError), + SaplingParse(sapling::pczt::ParseError), + SaplingSign(sapling::pczt::SignerError), + TransparentExtract(transparent::pczt::TxExtractorError), + TransparentParse(transparent::pczt::ParseError), + TransparentSign(transparent::pczt::SignerError), +} + +#[derive(Debug)] +pub enum GlobalError { + UnknownConsensusBranchId, + UnsupportedTxVersion { version: u32, version_group_id: u32 }, +} diff --git a/pczt/src/roles/spend_finalizer/mod.rs b/pczt/src/roles/spend_finalizer/mod.rs new file mode 100644 index 0000000000..7acf1e4137 --- /dev/null +++ b/pczt/src/roles/spend_finalizer/mod.rs @@ -0,0 +1,44 @@ +use zcash_primitives::transaction::components::transparent; + +use crate::Pczt; + +pub struct SpendFinalizer { + pczt: Pczt, +} + +impl SpendFinalizer { + /// Instantiates the Spend Finalizer role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Finalizes the spends of the PCZT. + pub fn finalize_spends(self) -> Result { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut transparent = transparent.into_parsed().map_err(Error::TransparentParse)?; + + transparent + .finalize_spends() + .map_err(Error::TransparentFinalize)?; + + Ok(Pczt { + global, + transparent: crate::transparent::Bundle::serialize_from(transparent), + sapling, + orchard, + }) + } +} + +/// Errors that can occur while finalizing the spends of a PCZT. +#[derive(Debug)] +pub enum Error { + TransparentFinalize(transparent::pczt::SpendFinalizerError), + TransparentParse(transparent::pczt::ParseError), +} diff --git a/pczt/src/roles/tx_extractor/mod.rs b/pczt/src/roles/tx_extractor/mod.rs new file mode 100644 index 0000000000..06d6fdf95a --- /dev/null +++ b/pczt/src/roles/tx_extractor/mod.rs @@ -0,0 +1,257 @@ +use std::marker::PhantomData; + +use rand_core::OsRng; +use zcash_primitives::{ + consensus::BranchId, + transaction::{ + sighash::{signature_hash, SignableInput}, + txid::TxIdDigester, + Authorization, Transaction, TransactionData, TxVersion, + }, +}; + +use crate::{Pczt, V5_TX_VERSION, V5_VERSION_GROUP_ID}; + +mod orchard; +pub use self::orchard::OrchardError; + +mod sapling; +pub use self::sapling::SaplingError; + +mod transparent; +pub use self::transparent::TransparentError; + +pub struct TransactionExtractor<'a> { + pczt: Pczt, + sapling_vk: Option<( + &'a ::sapling::circuit::SpendVerifyingKey, + &'a ::sapling::circuit::OutputVerifyingKey, + )>, + orchard_vk: Option<&'a ::orchard::circuit::VerifyingKey>, + _unused: PhantomData<&'a ()>, +} + +impl<'a> TransactionExtractor<'a> { + /// Instantiates the Transaction Extractor role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { + pczt, + sapling_vk: None, + orchard_vk: None, + _unused: PhantomData, + } + } + + /// Provides the Sapling Spend and Output verifying keys for validating the Sapling + /// proofs (if any). + /// + /// If not provided, and the PCZT has a Sapling bundle, [`Self::extract`] will return + /// an error. + pub fn with_sapling( + mut self, + spend_vk: &'a ::sapling::circuit::SpendVerifyingKey, + output_vk: &'a ::sapling::circuit::OutputVerifyingKey, + ) -> Self { + self.sapling_vk = Some((spend_vk, output_vk)); + self + } + + /// Provides an existing Orchard verifying key for validating the Orchard proof (if + /// any). + /// + /// If not provided, and the PCZT has an Orchard bundle, an Orchard verifying key will + /// be generated on the fly. + pub fn with_orchard(mut self, orchard_vk: &'a ::orchard::circuit::VerifyingKey) -> Self { + self.orchard_vk = Some(orchard_vk); + self + } + + /// Attempts to extract a valid transaction from the PCZT. + pub fn extract(self) -> Result { + let Self { + pczt, + sapling_vk, + orchard_vk, + _unused, + } = self; + + let version = match (pczt.global.tx_version, pczt.global.version_group_id) { + (V5_TX_VERSION, V5_VERSION_GROUP_ID) => Ok(TxVersion::Zip225), + (version, version_group_id) => Err(Error::Global(GlobalError::UnsupportedTxVersion { + version, + version_group_id, + })), + }?; + + let consensus_branch_id = BranchId::try_from(pczt.global.consensus_branch_id) + .map_err(|_| Error::Global(GlobalError::UnknownConsensusBranchId))?; + + let lock_time = determine_lock_time(&pczt.global, &pczt.transparent.inputs) + .map_err(|_| Error::IncompatibleLockTimes)?; + + let transparent_bundle = + transparent::extract_bundle(pczt.transparent).map_err(Error::Transparent)?; + let sapling_bundle = sapling::extract_bundle(pczt.sapling).map_err(Error::Sapling)?; + let orchard_bundle = orchard::extract_bundle(pczt.orchard).map_err(Error::Orchard)?; + + let tx_data = TransactionData::::from_parts( + version, + consensus_branch_id, + lock_time, + pczt.global.expiry_height.into(), + transparent_bundle, + None, + sapling_bundle, + orchard_bundle, + ); + + // The commitment being signed is shared across all shielded inputs. + let txid_parts = tx_data.digest(TxIdDigester); + let shielded_sighash = signature_hash(&tx_data, &SignableInput::Shielded, &txid_parts); + + // Create the binding signatures. + let tx_data = tx_data.try_map_bundles( + |t| Ok(t.map(|t| t.map_authorization(transparent::RemoveInputInfo))), + |s| { + s.map(|s| { + s.apply_binding_signature(*shielded_sighash.as_ref(), OsRng) + .ok_or(Error::SighashMismatch) + }) + .transpose() + }, + |o| { + o.map(|o| { + o.apply_binding_signature(*shielded_sighash.as_ref(), OsRng) + .ok_or(Error::SighashMismatch) + }) + .transpose() + }, + )?; + + let tx = tx_data.freeze().expect("v5 tx can't fail here"); + + // Now that we have a supposedly fully-authorized transaction, verify it. + if let Some(bundle) = tx.sapling_bundle() { + let (spend_vk, output_vk) = sapling_vk.ok_or(Error::SaplingRequired)?; + + sapling::verify_bundle(bundle, spend_vk, output_vk, *shielded_sighash.as_ref()) + .map_err(Error::Sapling)?; + } + if let Some(bundle) = tx.orchard_bundle() { + orchard::verify_bundle(bundle, orchard_vk, *shielded_sighash.as_ref()) + .map_err(Error::Orchard)?; + } + + Ok(tx) + } +} + +struct Unbound; + +impl Authorization for Unbound { + type TransparentAuth = zcash_primitives::transaction::components::transparent::pczt::Unbound; + type SaplingAuth = ::sapling::pczt::Unbound; + type OrchardAuth = ::orchard::pczt::Unbound; +} + +/// Errors that can occur while extracting a transaction from a PCZT. +#[derive(Debug)] +pub enum Error { + Global(GlobalError), + IncompatibleLockTimes, + Orchard(OrchardError), + Sapling(SaplingError), + SaplingRequired, + SighashMismatch, + Transparent(TransparentError), +} + +#[derive(Debug)] +pub enum GlobalError { + UnknownConsensusBranchId, + UnsupportedTxVersion { version: u32, version_group_id: u32 }, +} + +/// Determines the lock time for the transaction. +/// +/// Implemented following the specification in [BIP 370], with the rationale that this +/// makes integration of PCZTs simpler for codebases that already support PSBTs. +/// +/// [BIP 370]: https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#determining-lock-time +pub(crate) fn determine_lock_time( + global: &crate::common::Global, + inputs: &[L], +) -> Result { + // The nLockTime field of a transaction is determined by inspecting the + // `Global.fallback_lock_time` and each input's `required_time_lock_time` and + // `required_height_lock_time` fields. + + // If one or more inputs have a `required_time_lock_time` or `required_height_lock_time`, + let have_required_lock_time = inputs.iter().any(|input| { + input.required_time_lock_time().is_some() || input.required_height_lock_time().is_some() + }); + // then the field chosen is the one which is supported by all of the inputs. This can + // be determined by looking at all of the inputs which specify a locktime in either of + // those fields, and choosing the field which is present in all of those inputs. + // Inputs not specifying a lock time field can take both types of lock times, as can + // those that specify both. + let time_lock_time_unsupported = inputs + .iter() + .any(|input| input.required_height_lock_time().is_some()); + let height_lock_time_unsupported = inputs + .iter() + .any(|input| input.required_time_lock_time().is_some()); + + // The lock time chosen is then the maximum value of the chosen type of lock time. + match ( + have_required_lock_time, + time_lock_time_unsupported, + height_lock_time_unsupported, + ) { + (true, true, true) => Err(()), + (true, false, true) => Ok(inputs + .iter() + .filter_map(|input| input.required_time_lock_time()) + .max() + .expect("iterator is non-empty because have_required_lock_time is true")), + // If a PSBT has both types of locktimes possible because one or more inputs + // specify both `required_time_lock_time` and `required_height_lock_time`, then a + // locktime determined by looking at the `required_height_lock_time` fields of the + // inputs must be chosen. + (true, _, false) => Ok(inputs + .iter() + .filter_map(|input| input.required_height_lock_time()) + .max() + .expect("iterator is non-empty because have_required_lock_time is true")), + // If none of the inputs have a `required_time_lock_time` and + // `required_height_lock_time`, then `Global.fallback_lock_time` must be used. If + // `Global.fallback_lock_time` is not provided, then it is assumed to be 0. + (false, _, _) => Ok(global.fallback_lock_time.unwrap_or(0)), + } +} + +pub(crate) trait LockTimeInput { + fn required_time_lock_time(&self) -> Option; + fn required_height_lock_time(&self) -> Option; +} + +impl LockTimeInput for crate::transparent::Input { + fn required_time_lock_time(&self) -> Option { + self.required_time_lock_time + } + + fn required_height_lock_time(&self) -> Option { + self.required_height_lock_time + } +} + +#[cfg(feature = "transparent")] +impl LockTimeInput for zcash_primitives::transaction::components::transparent::pczt::Input { + fn required_time_lock_time(&self) -> Option { + *self.required_time_lock_time() + } + + fn required_height_lock_time(&self) -> Option { + *self.required_height_lock_time() + } +} diff --git a/pczt/src/roles/tx_extractor/orchard.rs b/pczt/src/roles/tx_extractor/orchard.rs new file mode 100644 index 0000000000..df99d57156 --- /dev/null +++ b/pczt/src/roles/tx_extractor/orchard.rs @@ -0,0 +1,46 @@ +use orchard::{bundle::Authorized, circuit::VerifyingKey, pczt::Unbound, Bundle}; +use rand_core::OsRng; +use zcash_protocol::value::ZatBalance; + +pub(super) fn extract_bundle( + bundle: crate::orchard::Bundle, +) -> Result>, OrchardError> { + bundle + .into_parsed() + .map_err(OrchardError::Parse)? + .extract() + .map_err(OrchardError::Extract) +} + +pub(super) fn verify_bundle( + bundle: &Bundle, + orchard_vk: Option<&VerifyingKey>, + sighash: [u8; 32], +) -> Result<(), OrchardError> { + let mut validator = orchard::bundle::BatchValidator::new(); + let rng = OsRng; + + validator.add_bundle(bundle, sighash); + + if let Some(vk) = orchard_vk { + if validator.validate(vk, rng) { + Ok(()) + } else { + Err(OrchardError::InvalidProof) + } + } else { + let vk = VerifyingKey::build(); + if validator.validate(&vk, rng) { + Ok(()) + } else { + Err(OrchardError::InvalidProof) + } + } +} + +#[derive(Debug)] +pub enum OrchardError { + Extract(orchard::pczt::TxExtractorError), + InvalidProof, + Parse(orchard::pczt::ParseError), +} diff --git a/pczt/src/roles/tx_extractor/sapling.rs b/pczt/src/roles/tx_extractor/sapling.rs new file mode 100644 index 0000000000..7d4562a29c --- /dev/null +++ b/pczt/src/roles/tx_extractor/sapling.rs @@ -0,0 +1,45 @@ +use rand_core::OsRng; +use sapling::{ + bundle::Authorized, + circuit::{OutputVerifyingKey, SpendVerifyingKey}, + pczt::Unbound, + BatchValidator, Bundle, +}; +use zcash_protocol::value::ZatBalance; + +pub(super) fn extract_bundle( + bundle: crate::sapling::Bundle, +) -> Result>, SaplingError> { + bundle + .into_parsed() + .map_err(SaplingError::Parse)? + .extract() + .map_err(SaplingError::Extract) +} + +pub(super) fn verify_bundle( + bundle: &Bundle, + spend_vk: &SpendVerifyingKey, + output_vk: &OutputVerifyingKey, + sighash: [u8; 32], +) -> Result<(), SaplingError> { + let mut validator = BatchValidator::new(); + + if !validator.check_bundle(bundle.clone(), sighash) { + return Err(SaplingError::ConsensusRuleViolation); + } + + if !validator.validate(spend_vk, output_vk, OsRng) { + return Err(SaplingError::InvalidProofsOrSignatures); + } + + Ok(()) +} + +#[derive(Debug)] +pub enum SaplingError { + ConsensusRuleViolation, + Extract(sapling::pczt::TxExtractorError), + InvalidProofsOrSignatures, + Parse(sapling::pczt::ParseError), +} diff --git a/pczt/src/roles/tx_extractor/transparent.rs b/pczt/src/roles/tx_extractor/transparent.rs new file mode 100644 index 0000000000..0c19c3bdab --- /dev/null +++ b/pczt/src/roles/tx_extractor/transparent.rs @@ -0,0 +1,35 @@ +use zcash_primitives::transaction::components::transparent::{ + pczt::{ParseError, TxExtractorError, Unbound}, + Authorization, Authorized, Bundle, MapAuth, +}; + +pub(super) fn extract_bundle( + bundle: crate::transparent::Bundle, +) -> Result>, TransparentError> { + bundle + .into_parsed() + .map_err(TransparentError::Parse)? + .extract() + .map_err(TransparentError::Extract) +} + +pub(super) struct RemoveInputInfo; + +impl MapAuth for RemoveInputInfo { + fn map_script_sig( + &self, + s: ::ScriptSig, + ) -> ::ScriptSig { + s + } + + fn map_authorization(&self, _: Unbound) -> Authorized { + Authorized + } +} + +#[derive(Debug)] +pub enum TransparentError { + Extract(TxExtractorError), + Parse(ParseError), +} diff --git a/pczt/src/roles/updater/mod.rs b/pczt/src/roles/updater/mod.rs new file mode 100644 index 0000000000..37c6d71a11 --- /dev/null +++ b/pczt/src/roles/updater/mod.rs @@ -0,0 +1,66 @@ +use crate::{common::Global, Pczt}; + +#[cfg(feature = "orchard")] +mod orchard; +#[cfg(feature = "orchard")] +pub use orchard::OrchardError; + +#[cfg(feature = "sapling")] +mod sapling; +#[cfg(feature = "sapling")] +pub use sapling::SaplingError; + +#[cfg(feature = "transparent")] +mod transparent; +#[cfg(feature = "transparent")] +pub use transparent::TransparentError; + +pub struct Updater { + pczt: Pczt, +} + +impl Updater { + /// Instantiates the Updater role with the given PCZT. + pub fn new(pczt: Pczt) -> Self { + Self { pczt } + } + + /// Updates the global transaction details with information in the given closure. + pub fn update_global_with(self, f: F) -> Self + where + F: FnOnce(GlobalUpdater<'_>), + { + let Pczt { + mut global, + transparent, + sapling, + orchard, + } = self.pczt; + + f(GlobalUpdater(&mut global)); + + Self { + pczt: Pczt { + global, + transparent, + sapling, + orchard, + }, + } + } + + /// Finishes the Updater role, returning the updated PCZT. + pub fn finish(self) -> Pczt { + self.pczt + } +} + +/// An updater for a transparent PCZT output. +pub struct GlobalUpdater<'a>(&'a mut Global); + +impl<'a> GlobalUpdater<'a> { + /// Stores the given proprietary value at the given key. + pub fn set_proprietary(&mut self, key: String, value: Vec) { + self.0.proprietary.insert(key, value); + } +} diff --git a/pczt/src/roles/updater/orchard.rs b/pczt/src/roles/updater/orchard.rs new file mode 100644 index 0000000000..5701b1c486 --- /dev/null +++ b/pczt/src/roles/updater/orchard.rs @@ -0,0 +1,38 @@ +use orchard::pczt::{ParseError, Updater, UpdaterError}; + +use crate::Pczt; + +impl super::Updater { + /// Updates the Orchard bundle with information in the given closure. + pub fn update_orchard_with(self, f: F) -> Result + where + F: FnOnce(Updater<'_>) -> Result<(), UpdaterError>, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = orchard.into_parsed().map_err(OrchardError::Parser)?; + + bundle.update_with(f).map_err(OrchardError::Updater)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling, + orchard: crate::orchard::Bundle::serialize_from(bundle), + }, + }) + } +} + +/// Errors that can occur while updating the Orchard bundle of a PCZT. +#[derive(Debug)] +pub enum OrchardError { + Parser(ParseError), + Updater(UpdaterError), +} diff --git a/pczt/src/roles/updater/sapling.rs b/pczt/src/roles/updater/sapling.rs new file mode 100644 index 0000000000..685ee11135 --- /dev/null +++ b/pczt/src/roles/updater/sapling.rs @@ -0,0 +1,38 @@ +use sapling::pczt::{ParseError, Updater, UpdaterError}; + +use crate::Pczt; + +impl super::Updater { + /// Updates the Sapling bundle with information in the given closure. + pub fn update_sapling_with(self, f: F) -> Result + where + F: FnOnce(Updater<'_>) -> Result<(), UpdaterError>, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = sapling.into_parsed().map_err(SaplingError::Parser)?; + + bundle.update_with(f).map_err(SaplingError::Updater)?; + + Ok(Self { + pczt: Pczt { + global, + transparent, + sapling: crate::sapling::Bundle::serialize_from(bundle), + orchard, + }, + }) + } +} + +/// Errors that can occur while updating the Sapling bundle of a PCZT. +#[derive(Debug)] +pub enum SaplingError { + Parser(ParseError), + Updater(UpdaterError), +} diff --git a/pczt/src/roles/updater/transparent.rs b/pczt/src/roles/updater/transparent.rs new file mode 100644 index 0000000000..8120654208 --- /dev/null +++ b/pczt/src/roles/updater/transparent.rs @@ -0,0 +1,42 @@ +use zcash_primitives::transaction::components::transparent::pczt::{ + ParseError, Updater, UpdaterError, +}; + +use crate::Pczt; + +impl super::Updater { + /// Updates the transparent bundle with information in the given closure. + pub fn update_transparent_with(self, f: F) -> Result + where + F: FnOnce(Updater<'_>) -> Result<(), UpdaterError>, + { + let Pczt { + global, + transparent, + sapling, + orchard, + } = self.pczt; + + let mut bundle = transparent + .into_parsed() + .map_err(TransparentError::Parser)?; + + bundle.update_with(f).map_err(TransparentError::Updater)?; + + Ok(Self { + pczt: Pczt { + global, + transparent: crate::transparent::Bundle::serialize_from(bundle), + sapling, + orchard, + }, + }) + } +} + +/// Errors that can occur while updating the transparent bundle of a PCZT. +#[derive(Debug)] +pub enum TransparentError { + Parser(ParseError), + Updater(UpdaterError), +} diff --git a/pczt/src/sapling.rs b/pczt/src/sapling.rs new file mode 100644 index 0000000000..1f6d34c5f1 --- /dev/null +++ b/pczt/src/sapling.rs @@ -0,0 +1,576 @@ +use std::cmp::Ordering; +use std::collections::BTreeMap; + +use getset::Getters; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +use crate::{ + common::{Global, Zip32Derivation}, + roles::combiner::{merge_map, merge_optional}, +}; + +const GROTH_PROOF_SIZE: usize = 48 + 96 + 48; + +/// PCZT fields that are specific to producing the transaction's Sapling bundle (if any). +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Bundle { + #[getset(get = "pub")] + pub(crate) spends: Vec, + #[getset(get = "pub")] + pub(crate) outputs: Vec, + + /// The net value of Sapling spends minus outputs. + /// + /// This is initialized by the Creator, and updated by the Constructor as spends or + /// outputs are added to the PCZT. It enables per-spend and per-output values to be + /// redacted from the PCZT after they are no longer necessary. + pub(crate) value_sum: i128, + + /// The Sapling anchor for this transaction. + /// + /// Set by the Creator. + pub(crate) anchor: [u8; 32], + + /// The Sapling binding signature signing key. + /// + /// - This is `None` until it is set by the IO Finalizer. + /// - The Transaction Extractor uses this to produce the binding signature. + pub(crate) bsk: Option<[u8; 32]>, +} + +/// Information about a Sapling spend within a transaction. +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Spend { + // + // SpendDescription effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + pub(crate) cv: [u8; 32], + pub(crate) nullifier: [u8; 32], + pub(crate) rk: [u8; 32], + + /// The Spend proof. + /// + /// This is set by the Prover. + #[serde_as(as = "Option<[_; GROTH_PROOF_SIZE]>")] + pub(crate) zkproof: Option<[u8; GROTH_PROOF_SIZE]>, + + /// The spend authorization signature. + /// + /// This is set by the Signer. + #[serde_as(as = "Option<[_; 64]>")] + pub(crate) spend_auth_sig: Option<[u8; 64]>, + + /// The [raw encoding] of the Sapling payment address that received the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// + /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding + #[serde_as(as = "Option<[_; 43]>")] + pub(crate) recipient: Option<[u8; 43]>, + + /// The value of the input being spent. + /// + /// This may be used by Signers to verify that the value matches `cv`, and to confirm + /// the values and change involved in the transaction. + /// + /// This exposes the input value to all participants. For Signers who don't need this + /// information, or after signatures have been applied, this can be redacted. + pub(crate) value: Option, + + /// The note commitment randomness. + /// + /// - This is set by the Constructor. It MUST NOT be set if the note has an `rseed` + /// (i.e. was created after [ZIP 212] activation). + /// - The Prover requires either this or `rseed`. + /// + /// [ZIP 212]: https://zips.z.cash/zip-0212 + pub(crate) rcm: Option<[u8; 32]>, + + /// The seed randomness for the note being spent. + /// + /// - This is set by the Constructor. It MUST NOT be set if the note has no `rseed` + /// (i.e. was created before [ZIP 212] activation). + /// - The Prover requires either this or `rcm`. + /// + /// [ZIP 212]: https://zips.z.cash/zip-0212 + pub(crate) rseed: Option<[u8; 32]>, + + /// The value commitment randomness. + /// + /// - This is set by the Constructor. + /// - The IO Finalizer compresses it into `bsk`. + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value correctly matches `cv`. + /// + /// This opens `cv` for all participants. For Signers who don't need this information, + /// or after proofs / signatures have been applied, this can be redacted. + pub(crate) rcv: Option<[u8; 32]>, + + /// The proof generation key `(ak, nsk)` corresponding to the recipient that received + /// the note being spent. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + pub(crate) proof_generation_key: Option<([u8; 32], [u8; 32])>, + + /// A witness from the note to the bundle's anchor. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + pub(crate) witness: Option<(u32, [[u8; 32]; 32])>, + + /// The spend authorization randomizer. + /// + /// - This is chosen by the Constructor. + /// - This is required by the Signer for creating `spend_auth_sig`, and may be used to + /// validate `rk`. + /// - After `zkproof` / `spend_auth_sig` has been set, this can be redacted. + pub(crate) alpha: Option<[u8; 32]>, + + /// The ZIP 32 derivation path at which the spending key can be found for the note + /// being spent. + pub(crate) zip32_derivation: Option, + + /// The spend authorizing key for this spent note, if it is a dummy note. + /// + /// - This is chosen by the Constructor. + /// - This is required by the IO Finalizer, and is cleared by it once used. + /// - Signers MUST reject PCZTs that contain `dummy_ask` values. + pub(crate) dummy_ask: Option<[u8; 32]>, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +/// Information about a Sapling output within a transaction. +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Output { + // + // OutputDescription effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + pub(crate) cv: [u8; 32], + pub(crate) cmu: [u8; 32], + pub(crate) ephemeral_key: [u8; 32], + /// The encrypted note plaintext for the output. + /// + /// Encoded as a `Vec` because its length depends on the transaction version. + /// + /// Once we have [memo bundles], we will be able to set memos independently of + /// Outputs. For now, the Constructor sets both at the same time. + /// + /// [memo bundles]: https://zips.z.cash/zip-0231 + pub(crate) enc_ciphertext: Vec, + /// The encrypted note plaintext for the output. + /// + /// Encoded as a `Vec` because its length depends on the transaction version. + pub(crate) out_ciphertext: Vec, + + /// The Output proof. + /// + /// This is set by the Prover. + #[serde_as(as = "Option<[_; GROTH_PROOF_SIZE]>")] + pub(crate) zkproof: Option<[u8; GROTH_PROOF_SIZE]>, + + /// The [raw encoding] of the Sapling payment address that will receive the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// + /// [raw encoding]: https://zips.z.cash/protocol/protocol.pdf#saplingpaymentaddrencoding + #[serde_as(as = "Option<[_; 43]>")] + #[getset(get = "pub")] + pub(crate) recipient: Option<[u8; 43]>, + + /// The value of the output. + /// + /// This may be used by Signers to verify that the value matches `cv`, and to confirm + /// the values and change involved in the transaction. + /// + /// This exposes the output value to all participants. For Signers who don't need this + /// information, or after signatures have been applied, this can be redacted. + #[getset(get = "pub")] + pub(crate) value: Option, + + /// The seed randomness for the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover, instead of disclosing `shared_secret` to them. + #[getset(get = "pub")] + pub(crate) rseed: Option<[u8; 32]>, + + /// The value commitment randomness. + /// + /// - This is set by the Constructor. + /// - The IO Finalizer compresses it into `bsk`. + /// - This is required by the Prover. + /// - This may be used by Signers to verify that the value correctly matches `cv`. + /// + /// This opens `cv` for all participants. For Signers who don't need this information, + /// or after proofs / signatures have been applied, this can be redacted. + pub(crate) rcv: Option<[u8; 32]>, + + /// The `ock` value used to encrypt `out_ciphertext`. + /// + /// This enables Signers to verify that `out_ciphertext` is correctly encrypted. + /// + /// This may be `None` if the Constructor added the output using an OVK policy of + /// "None", to make the output unrecoverable from the chain by the sender. + pub(crate) ock: Option<[u8; 32]>, + + /// The ZIP 32 derivation path at which the spending key can be found for the output. + pub(crate) zip32_derivation: Option, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +impl Bundle { + /// Merges this bundle with another. + /// + /// Returns `None` if the bundles have conflicting data. + pub(crate) fn merge( + mut self, + other: Self, + self_global: &Global, + other_global: &Global, + ) -> Option { + // Destructure `other` to ensure we handle everything. + let Self { + mut spends, + mut outputs, + value_sum, + anchor, + bsk, + } = other; + + // If `bsk` is set on either bundle, the IO Finalizer has run, which means we + // cannot have differing numbers of spends or outputs, and the value balances must + // match. + match (self.bsk.as_mut(), bsk) { + (Some(lhs), Some(rhs)) if lhs != &rhs => return None, + (Some(_), _) | (_, Some(_)) + if self.spends.len() != spends.len() + || self.outputs.len() != outputs.len() + || self.value_sum != value_sum => + { + return None + } + // IO Finalizer has run, and neither bundle has excess spends or outputs. + (Some(_), _) | (_, Some(_)) => (), + // IO Finalizer has not run on either bundle. + (None, None) => { + let (spends_cmp_other, outputs_cmp_other) = match ( + self.spends.len().cmp(&spends.len()), + self.outputs.len().cmp(&outputs.len()), + ) { + // These cases require us to recalculate the value sum, which we can't + // do without a parsed bundle. + (Ordering::Less, Ordering::Greater) | (Ordering::Greater, Ordering::Less) => { + return None + } + // These cases mean that at least one of the two value sums is correct + // and we can use it directly. + (spends, outputs) => (spends, outputs), + }; + + match ( + self_global.shielded_modifiable(), + other_global.shielded_modifiable(), + spends_cmp_other, + ) { + // Fail if the merge would add spends to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more spends than us, move them over; these cannot + // conflict by construction. + (true, _, Ordering::Less) => { + self.spends.extend(spends.drain(self.spends.len()..)) + } + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + } + + match ( + self_global.shielded_modifiable(), + other_global.shielded_modifiable(), + outputs_cmp_other, + ) { + // Fail if the merge would add outputs to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more outputs than us, move them over; these cannot + // conflict by construction. + (true, _, Ordering::Less) => { + self.outputs.extend(outputs.drain(self.outputs.len()..)) + } + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + } + + if matches!(spends_cmp_other, Ordering::Less) + || matches!(outputs_cmp_other, Ordering::Less) + { + // We check below that the overlapping spends and outputs match. + // Assuming here that they will, we take the other bundle's value sum. + self.value_sum = value_sum; + } + } + } + + if self.anchor != anchor { + return None; + } + + // Leverage the early-exit behaviour of zip to confirm that the remaining data in + // the other bundle matches this one. + for (lhs, rhs) in self.spends.iter_mut().zip(spends.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Spend { + cv, + nullifier, + rk, + zkproof, + spend_auth_sig, + recipient, + value, + rcm, + rseed, + rcv, + proof_generation_key, + witness, + alpha, + zip32_derivation, + dummy_ask, + proprietary, + } = rhs; + + if lhs.cv != cv || lhs.nullifier != nullifier || lhs.rk != rk { + return None; + } + + if !(merge_optional(&mut lhs.zkproof, zkproof) + && merge_optional(&mut lhs.spend_auth_sig, spend_auth_sig) + && merge_optional(&mut lhs.recipient, recipient) + && merge_optional(&mut lhs.value, value) + && merge_optional(&mut lhs.rcm, rcm) + && merge_optional(&mut lhs.rseed, rseed) + && merge_optional(&mut lhs.rcv, rcv) + && merge_optional(&mut lhs.proof_generation_key, proof_generation_key) + && merge_optional(&mut lhs.witness, witness) + && merge_optional(&mut lhs.alpha, alpha) + && merge_optional(&mut lhs.zip32_derivation, zip32_derivation) + && merge_optional(&mut lhs.dummy_ask, dummy_ask) + && merge_map(&mut lhs.proprietary, proprietary)) + { + return None; + } + } + + for (lhs, rhs) in self.outputs.iter_mut().zip(outputs.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Output { + cv, + cmu, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + zkproof, + recipient, + value, + rseed, + rcv, + ock, + zip32_derivation, + proprietary, + } = rhs; + + if lhs.cv != cv + || lhs.cmu != cmu + || lhs.ephemeral_key != ephemeral_key + || lhs.enc_ciphertext != enc_ciphertext + || lhs.out_ciphertext != out_ciphertext + { + return None; + } + + if !(merge_optional(&mut lhs.zkproof, zkproof) + && merge_optional(&mut lhs.recipient, recipient) + && merge_optional(&mut lhs.value, value) + && merge_optional(&mut lhs.rseed, rseed) + && merge_optional(&mut lhs.rcv, rcv) + && merge_optional(&mut lhs.ock, ock) + && merge_optional(&mut lhs.zip32_derivation, zip32_derivation) + && merge_map(&mut lhs.proprietary, proprietary)) + { + return None; + } + } + + Some(self) + } +} + +#[cfg(feature = "sapling")] +impl Bundle { + pub(crate) fn into_parsed(self) -> Result { + let spends = self + .spends + .into_iter() + .map(|spend| { + sapling::pczt::Spend::parse( + spend.cv, + spend.nullifier, + spend.rk, + spend.zkproof, + spend.spend_auth_sig, + spend.recipient, + spend.value, + spend.rcm, + spend.rseed, + spend.rcv, + spend.proof_generation_key, + spend.witness, + spend.alpha, + spend + .zip32_derivation + .map(|z| { + sapling::pczt::Zip32Derivation::parse( + z.seed_fingerprint, + z.derivation_path, + ) + }) + .transpose()?, + spend.dummy_ask, + spend.proprietary, + ) + }) + .collect::>()?; + + let outputs = self + .outputs + .into_iter() + .map(|output| { + sapling::pczt::Output::parse( + output.cv, + output.cmu, + output.ephemeral_key, + output.enc_ciphertext, + output.out_ciphertext, + output.zkproof, + output.recipient, + output.value, + output.rseed, + output.rcv, + output.ock, + output + .zip32_derivation + .map(|z| { + sapling::pczt::Zip32Derivation::parse( + z.seed_fingerprint, + z.derivation_path, + ) + }) + .transpose()?, + output.proprietary, + ) + }) + .collect::>()?; + + sapling::pczt::Bundle::parse(spends, outputs, self.value_sum, self.anchor, self.bsk) + } + + pub(crate) fn serialize_from(bundle: sapling::pczt::Bundle) -> Self { + let spends = bundle + .spends() + .iter() + .map(|spend| { + let (rcm, rseed) = match spend.rseed() { + Some(sapling::Rseed::BeforeZip212(rcm)) => (Some(rcm.to_bytes()), None), + Some(sapling::Rseed::AfterZip212(rseed)) => (None, Some(*rseed)), + None => (None, None), + }; + + Spend { + cv: spend.cv().to_bytes(), + nullifier: spend.nullifier().0, + rk: (*spend.rk()).into(), + zkproof: *spend.zkproof(), + spend_auth_sig: spend.spend_auth_sig().map(|s| s.into()), + recipient: spend.recipient().map(|recipient| recipient.to_bytes()), + value: spend.value().map(|value| value.inner()), + rcm, + rseed, + rcv: spend.rcv().as_ref().map(|rcv| rcv.inner().to_bytes()), + proof_generation_key: spend + .proof_generation_key() + .as_ref() + .map(|key| (key.ak.to_bytes(), key.nsk.to_bytes())), + witness: spend.witness().as_ref().map(|witness| { + ( + u32::try_from(u64::from(witness.position())) + .expect("Sapling positions fit in u32"), + witness + .path_elems() + .iter() + .map(|node| node.to_bytes()) + .collect::>()[..] + .try_into() + .expect("path is length 32"), + ) + }), + alpha: spend.alpha().map(|alpha| alpha.to_bytes()), + zip32_derivation: spend.zip32_derivation().as_ref().map(|z| Zip32Derivation { + seed_fingerprint: *z.seed_fingerprint(), + derivation_path: z.derivation_path().iter().map(|i| i.index()).collect(), + }), + dummy_ask: spend + .dummy_ask() + .as_ref() + .map(|dummy_ask| dummy_ask.to_bytes()), + proprietary: spend.proprietary().clone(), + } + }) + .collect(); + + let outputs = bundle + .outputs() + .iter() + .map(|output| Output { + cv: output.cv().to_bytes(), + cmu: output.cmu().to_bytes(), + ephemeral_key: output.ephemeral_key().0, + enc_ciphertext: output.enc_ciphertext().to_vec(), + out_ciphertext: output.out_ciphertext().to_vec(), + zkproof: *output.zkproof(), + recipient: output.recipient().map(|recipient| recipient.to_bytes()), + value: output.value().map(|value| value.inner()), + rseed: *output.rseed(), + rcv: output.rcv().as_ref().map(|rcv| rcv.inner().to_bytes()), + ock: output.ock().as_ref().map(|ock| ock.0), + zip32_derivation: output.zip32_derivation().as_ref().map(|z| Zip32Derivation { + seed_fingerprint: *z.seed_fingerprint(), + derivation_path: z.derivation_path().iter().map(|i| i.index()).collect(), + }), + proprietary: output.proprietary().clone(), + }) + .collect(); + + Self { + spends, + outputs, + value_sum: bundle.value_sum().to_raw(), + anchor: bundle.anchor().to_bytes(), + bsk: bundle.bsk().map(|bsk| bsk.into()), + } + } +} diff --git a/pczt/src/transparent.rs b/pczt/src/transparent.rs new file mode 100644 index 0000000000..311d8d571d --- /dev/null +++ b/pczt/src/transparent.rs @@ -0,0 +1,439 @@ +use std::{cmp::Ordering, collections::BTreeMap}; + +use crate::{ + common::{Global, Zip32Derivation}, + roles::combiner::{merge_map, merge_optional}, +}; + +use getset::Getters; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; + +#[cfg(feature = "transparent")] +use zcash_primitives::transaction::components::transparent; + +/// PCZT fields that are specific to producing the transaction's transparent bundle (if +/// any). +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Bundle { + #[getset(get = "pub")] + pub(crate) inputs: Vec, + #[getset(get = "pub")] + pub(crate) outputs: Vec, +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Input { + // + // Transparent effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + #[getset(get = "pub")] + pub(crate) prevout_txid: [u8; 32], + #[getset(get = "pub")] + pub(crate) prevout_index: u32, + + /// The sequence number of this input. + /// + /// - This is set by the Constructor. + /// - If omitted, the sequence number is assumed to be the final sequence number + /// (`0xffffffff`). + pub(crate) sequence: Option, + + /// The minimum Unix timstamp that this input requires to be set as the transaction's + /// lock time. + /// + /// - This is set by the Constructor. + /// - This must be greater than or equal to 500000000. + pub(crate) required_time_lock_time: Option, + + /// The minimum block height that this input requires to be set as the transaction's + /// lock time. + /// + /// - This is set by the Constructor. + /// - This must be greater than 0 and less than 500000000. + pub(crate) required_height_lock_time: Option, + + /// A satisfying witness for the `script_pubkey` of the input being spent. + /// + /// This is set by the Spend Finalizer. + pub(crate) script_sig: Option>, + + // These are required by the Transaction Extractor, to derive the shielded sighash + // needed for computing the binding signatures. + #[getset(get = "pub")] + pub(crate) value: u64, + pub(crate) script_pubkey: Vec, + + /// The script required to spend this output, if it is P2SH. + /// + /// Set to `None` if this is a P2PKH output. + pub(crate) redeem_script: Option>, + + /// A map from a pubkey to a signature created by it. + /// + /// - Each pubkey should appear in `script_pubkey` or `redeem_script`. + /// - Each entry is set by a Signer, and should contain an ECDSA signature that is + /// valid under the corresponding pubkey. + /// - These are required by the Spend Finalizer to assemble `script_sig`. + #[serde_as(as = "BTreeMap<[_; 33], _>")] + pub(crate) partial_signatures: BTreeMap<[u8; 33], Vec>, + + /// The sighash type to be used for this input. + /// + /// - Signers must use this sighash type to produce their signatures. Signers that + /// cannot produce signatures for this sighash type must not provide a signature. + /// - Spend Finalizers must fail to finalize inputs which have signatures not matching + /// this sighash type. + pub(crate) sighash_type: u8, + + /// A map from a pubkey to the BIP 32 derivation path at which its corresponding + /// spending key can be found. + /// + /// - The pubkeys should appear in `script_pubkey` or `redeem_script`. + /// - Each entry is set by an Updater. + /// - Individual entries may be required by a Signer. + /// - It is not required that the map include entries for all of the used pubkeys. + /// In particular, it is not possible to include entries for non-BIP-32 pubkeys. + #[serde_as(as = "BTreeMap<[_; 33], _>")] + pub(crate) bip32_derivation: BTreeMap<[u8; 33], Zip32Derivation>, + + /// Mappings of the form `key = RIPEMD160(value)`. + /// + /// - These may be used by the Signer to inspect parts of `script_pubkey` or + /// `redeem_script`. + pub(crate) ripemd160_preimages: BTreeMap<[u8; 20], Vec>, + + /// Mappings of the form `key = SHA256(value)`. + /// + /// - These may be used by the Signer to inspect parts of `script_pubkey` or + /// `redeem_script`. + pub(crate) sha256_preimages: BTreeMap<[u8; 32], Vec>, + + /// Mappings of the form `key = RIPEMD160(SHA256(value))`. + /// + /// - These may be used by the Signer to inspect parts of `script_pubkey` or + /// `redeem_script`. + pub(crate) hash160_preimages: BTreeMap<[u8; 20], Vec>, + + /// Mappings of the form `key = SHA256(SHA256(value))`. + /// + /// - These may be used by the Signer to inspect parts of `script_pubkey` or + /// `redeem_script`. + pub(crate) hash256_preimages: BTreeMap<[u8; 32], Vec>, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize, Getters)] +pub struct Output { + // + // Transparent effecting data. + // + // These are required fields that are part of the final transaction, and are filled in + // by the Constructor when adding an output. + // + pub(crate) value: u64, + pub(crate) script_pubkey: Vec, + + /// The script required to spend this output, if it is P2SH. + /// + /// Set to `None` if this is a P2PKH output. + pub(crate) redeem_script: Option>, + + /// A map from a pubkey to the BIP 32 derivation path at which its corresponding + /// spending key can be found. + /// + /// - The pubkeys should appear in `script_pubkey` or `redeem_script`. + /// - Each entry is set by an Updater. + /// - Individual entries may be required by a Signer. + /// - It is not required that the map include entries for all of the used pubkeys. + /// In particular, it is not possible to include entries for non-BIP-32 pubkeys. + #[serde_as(as = "BTreeMap<[_; 33], _>")] + pub(crate) bip32_derivation: BTreeMap<[u8; 33], Zip32Derivation>, + + /// Proprietary fields related to the note being spent. + #[getset(get = "pub")] + pub(crate) proprietary: BTreeMap>, +} + +impl Bundle { + /// Merges this bundle with another. + /// + /// Returns `None` if the bundles have conflicting data. + pub(crate) fn merge( + mut self, + other: Self, + self_global: &Global, + other_global: &Global, + ) -> Option { + // Destructure `other` to ensure we handle everything. + let Self { + mut inputs, + mut outputs, + } = other; + + match ( + self_global.inputs_modifiable(), + other_global.inputs_modifiable(), + self.inputs.len().cmp(&inputs.len()), + ) { + // Fail if the merge would add inputs to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more inputs than us, move them over; these cannot + // conflict by construction. + (true, _, Ordering::Less) => self.inputs.extend(inputs.drain(self.inputs.len()..)), + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + } + + match ( + self_global.outputs_modifiable(), + other_global.outputs_modifiable(), + self.outputs.len().cmp(&outputs.len()), + ) { + // Fail if the merge would add outputs to a non-modifiable bundle. + (false, _, Ordering::Less) | (_, false, Ordering::Greater) => return None, + // If the other bundle has more outputs than us, move them over; these cannot + // conflict by construction. + (true, _, Ordering::Less) => self.outputs.extend(outputs.drain(self.outputs.len()..)), + // Do nothing otherwise. + (_, _, Ordering::Equal) | (_, true, Ordering::Greater) => (), + } + + // Leverage the early-exit behaviour of zip to confirm that the remaining data in + // the other bundle matches this one. + for (lhs, rhs) in self.inputs.iter_mut().zip(inputs.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Input { + prevout_txid, + prevout_index, + sequence, + required_time_lock_time, + required_height_lock_time, + script_sig, + value, + script_pubkey, + redeem_script, + partial_signatures, + sighash_type, + bip32_derivation, + ripemd160_preimages, + sha256_preimages, + hash160_preimages, + hash256_preimages, + proprietary, + } = rhs; + + if lhs.prevout_txid != prevout_txid + || lhs.prevout_index != prevout_index + || lhs.value != value + || lhs.script_pubkey != script_pubkey + || lhs.sighash_type != sighash_type + { + return None; + } + + if !(merge_optional(&mut lhs.sequence, sequence) + && merge_optional(&mut lhs.required_time_lock_time, required_time_lock_time) + && merge_optional( + &mut lhs.required_height_lock_time, + required_height_lock_time, + ) + && merge_optional(&mut lhs.script_sig, script_sig) + && merge_optional(&mut lhs.redeem_script, redeem_script) + && merge_map(&mut lhs.partial_signatures, partial_signatures) + && merge_map(&mut lhs.bip32_derivation, bip32_derivation) + && merge_map(&mut lhs.ripemd160_preimages, ripemd160_preimages) + && merge_map(&mut lhs.sha256_preimages, sha256_preimages) + && merge_map(&mut lhs.hash160_preimages, hash160_preimages) + && merge_map(&mut lhs.hash256_preimages, hash256_preimages) + && merge_map(&mut lhs.proprietary, proprietary)) + { + return None; + } + } + + for (lhs, rhs) in self.outputs.iter_mut().zip(outputs.into_iter()) { + // Destructure `rhs` to ensure we handle everything. + let Output { + value, + script_pubkey, + redeem_script, + bip32_derivation, + proprietary, + } = rhs; + + if lhs.value != value || lhs.script_pubkey != script_pubkey { + return None; + } + + if !(merge_optional(&mut lhs.redeem_script, redeem_script) + && merge_map(&mut lhs.bip32_derivation, bip32_derivation) + && merge_map(&mut lhs.proprietary, proprietary)) + { + return None; + } + } + + Some(self) + } +} + +#[cfg(feature = "transparent")] +impl Bundle { + pub(crate) fn into_parsed( + self, + ) -> Result { + let inputs = self + .inputs + .into_iter() + .map(|input| { + transparent::pczt::Input::parse( + input.prevout_txid, + input.prevout_index, + input.sequence, + input.required_time_lock_time, + input.required_height_lock_time, + input.script_sig, + input.value, + input.script_pubkey, + input.redeem_script, + input.partial_signatures, + input.sighash_type, + input + .bip32_derivation + .into_iter() + .map(|(k, v)| { + transparent::pczt::Bip32Derivation::parse( + v.seed_fingerprint, + v.derivation_path, + ) + .map(|v| (k, v)) + }) + .collect::>()?, + input.ripemd160_preimages, + input.sha256_preimages, + input.hash160_preimages, + input.hash256_preimages, + input.proprietary, + ) + }) + .collect::>()?; + + let outputs = self + .outputs + .into_iter() + .map(|output| { + transparent::pczt::Output::parse( + output.value, + output.script_pubkey, + output.redeem_script, + output + .bip32_derivation + .into_iter() + .map(|(k, v)| { + transparent::pczt::Bip32Derivation::parse( + v.seed_fingerprint, + v.derivation_path, + ) + .map(|v| (k, v)) + }) + .collect::>()?, + output.proprietary, + ) + }) + .collect::>()?; + + transparent::pczt::Bundle::parse(inputs, outputs) + } + + pub(crate) fn serialize_from(bundle: transparent::pczt::Bundle) -> Self { + let inputs = bundle + .inputs() + .iter() + .map(|input| Input { + prevout_txid: (*input.prevout_txid()).into(), + prevout_index: *input.prevout_index(), + sequence: *input.sequence(), + required_time_lock_time: *input.required_time_lock_time(), + required_height_lock_time: *input.required_height_lock_time(), + script_sig: input + .script_sig() + .as_ref() + .map(|script_sig| script_sig.0.clone()), + value: input.value().into_u64(), + script_pubkey: input.script_pubkey().0.clone(), + redeem_script: input + .redeem_script() + .as_ref() + .map(|redeem_script| redeem_script.0.clone()), + partial_signatures: input.partial_signatures().clone(), + sighash_type: input.sighash_type().encode(), + bip32_derivation: input + .bip32_derivation() + .iter() + .map(|(k, v)| { + ( + *k, + Zip32Derivation { + seed_fingerprint: *v.seed_fingerprint(), + derivation_path: v + .derivation_path() + .iter() + .copied() + .map(u32::from) + .collect(), + }, + ) + }) + .collect(), + ripemd160_preimages: input.ripemd160_preimages().clone(), + sha256_preimages: input.sha256_preimages().clone(), + hash160_preimages: input.hash160_preimages().clone(), + hash256_preimages: input.hash256_preimages().clone(), + proprietary: input.proprietary().clone(), + }) + .collect(); + + let outputs = bundle + .outputs() + .iter() + .map(|output| Output { + value: output.value().into_u64(), + script_pubkey: output.script_pubkey().0.clone(), + redeem_script: output + .redeem_script() + .as_ref() + .map(|redeem_script| redeem_script.0.clone()), + bip32_derivation: output + .bip32_derivation() + .iter() + .map(|(k, v)| { + ( + *k, + Zip32Derivation { + seed_fingerprint: *v.seed_fingerprint(), + derivation_path: v + .derivation_path() + .iter() + .copied() + .map(u32::from) + .collect(), + }, + ) + }) + .collect(), + proprietary: output.proprietary().clone(), + }) + .collect(); + + Self { inputs, outputs } + } +} diff --git a/pczt/tests/end_to_end.rs b/pczt/tests/end_to_end.rs new file mode 100644 index 0000000000..e2e59b30ed --- /dev/null +++ b/pczt/tests/end_to_end.rs @@ -0,0 +1,405 @@ +use std::sync::OnceLock; + +use orchard::tree::MerkleHashOrchard; +use pczt::{ + roles::{ + combiner::Combiner, creator::Creator, io_finalizer::IoFinalizer, prover::Prover, + signer::Signer, spend_finalizer::SpendFinalizer, tx_extractor::TransactionExtractor, + }, + Pczt, +}; +use rand_core::OsRng; +use shardtree::{store::memory::MemoryShardStore, ShardTree}; +use zcash_note_encryption::try_note_decryption; +use zcash_primitives::{ + legacy::keys::{AccountPrivKey, IncomingViewingKey}, + transaction::{ + builder::{BuildConfig, Builder, PcztResult}, + components::transparent, + fees::zip317, + }, +}; +use zcash_proofs::prover::LocalTxProver; +use zcash_protocol::{consensus::MainNetwork, memo::MemoBytes, value::Zatoshis}; + +static ORCHARD_PROVING_KEY: OnceLock = OnceLock::new(); + +fn orchard_proving_key() -> &'static orchard::circuit::ProvingKey { + ORCHARD_PROVING_KEY.get_or_init(orchard::circuit::ProvingKey::build) +} + +fn check_round_trip(pczt: &Pczt) { + let encoded = pczt.serialize(); + assert_eq!(encoded, Pczt::parse(&encoded).unwrap().serialize()); +} + +#[test] +fn transparent_to_orchard() { + let params = MainNetwork; + let rng = OsRng; + + // Create a transparent account to send funds from. + let transparent_account_sk = + AccountPrivKey::from_seed(¶ms, &[1; 32], zip32::AccountId::ZERO).unwrap(); + let (transparent_addr, address_index) = transparent_account_sk + .to_account_pubkey() + .derive_external_ivk() + .unwrap() + .default_address(); + let transparent_sk = transparent_account_sk + .derive_external_secret_key(address_index) + .unwrap(); + + // Create an Orchard account to receive funds. + let orchard_sk = orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(); + let orchard_fvk = orchard::keys::FullViewingKey::from(&orchard_sk); + let orchard_ovk = orchard_fvk.to_ovk(orchard::keys::Scope::External); + let recipient = orchard_fvk.address_at(0u32, orchard::keys::Scope::External); + + // Pretend we already have a transparent coin. + let utxo = transparent::OutPoint::fake(); + let coin = transparent::TxOut { + value: Zatoshis::const_from_u64(1_000_000), + script_pubkey: transparent_addr.script(), + }; + + // Create the transaction's I/O. + let mut builder = Builder::new( + params, + 10_000_000.into(), + BuildConfig::Standard { + sapling_anchor: None, + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }, + ); + builder + .add_transparent_input(transparent_sk, utxo, coin) + .unwrap(); + builder + .add_orchard_output::( + Some(orchard_ovk), + recipient, + 100_000, + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_orchard_output::( + Some(orchard_fvk.to_ovk(zip32::Scope::Internal)), + orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal), + 885_000, + MemoBytes::empty(), + ) + .unwrap(); + let PcztResult { pczt_parts, .. } = builder + .build_for_pczt(rng, &zip317::FeeRule::standard()) + .unwrap(); + + // Create the base PCZT. + let pczt = Creator::build_from_parts(pczt_parts).unwrap(); + check_round_trip(&pczt); + + // Finalize the I/O. + let pczt = IoFinalizer::new(pczt).finalize_io().unwrap(); + check_round_trip(&pczt); + + // Create proofs. + let pczt = Prover::new(pczt) + .create_orchard_proof(orchard_proving_key()) + .unwrap() + .finish(); + check_round_trip(&pczt); + + // Apply signatures. + let mut signer = Signer::new(pczt).unwrap(); + signer.sign_transparent(0, &transparent_sk).unwrap(); + let pczt = signer.finish(); + check_round_trip(&pczt); + + // Finalize spends. + let pczt = SpendFinalizer::new(pczt).finalize_spends().unwrap(); + check_round_trip(&pczt); + + // We should now be able to extract the fully authorized transaction. + let tx = TransactionExtractor::new(pczt).extract().unwrap(); + + assert_eq!(u32::from(tx.expiry_height()), 10_000_040); + + // TODO: Validate the transaction. +} + +#[test] +fn sapling_to_orchard() { + let mut rng = OsRng; + + // Create a Sapling account to send funds from. + let sapling_extsk = sapling::zip32::ExtendedSpendingKey::master(&[1; 32]); + let sapling_dfvk = sapling_extsk.to_diversifiable_full_viewing_key(); + let sapling_internal_dfvk = sapling_extsk + .derive_internal() + .to_diversifiable_full_viewing_key(); + let sapling_recipient = sapling_dfvk.default_address().1; + + // Create an Orchard account to receive funds. + let orchard_sk = orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(); + let orchard_fvk = orchard::keys::FullViewingKey::from(&orchard_sk); + let recipient = orchard_fvk.address_at(0u32, orchard::keys::Scope::External); + + // Pretend we already received a note. + let value = sapling::value::NoteValue::from_raw(1_000_000); + let note = { + let mut sapling_builder = sapling::builder::Builder::new( + sapling::note_encryption::Zip212Enforcement::On, + sapling::builder::BundleType::DEFAULT, + sapling::Anchor::empty_tree(), + ); + sapling_builder + .add_output(None, sapling_recipient, value, None) + .unwrap(); + let (bundle, meta) = sapling_builder + .build::(&mut rng) + .unwrap() + .unwrap(); + let output = bundle + .shielded_outputs() + .get(meta.output_index(0).unwrap()) + .unwrap(); + let domain = sapling::note_encryption::SaplingDomain::new( + sapling::note_encryption::Zip212Enforcement::On, + ); + let (note, _, _) = + try_note_decryption(&domain, &sapling_dfvk.to_external_ivk().prepare(), output) + .unwrap(); + note + }; + + // Use the tree with a single leaf. + let (anchor, merkle_path) = { + let cmu = note.cmu(); + let leaf = sapling::Node::from_cmu(&cmu); + let mut tree = + ShardTree::<_, 32, 16>::new(MemoryShardStore::::empty(), 100); + tree.append(leaf, incrementalmerkletree::Retention::Marked) + .unwrap(); + tree.checkpoint(9_999_999).unwrap(); + let position = 0.into(); + let merkle_path = tree + .witness_at_checkpoint_depth(position, 0) + .unwrap() + .unwrap(); + let anchor = merkle_path.root(leaf); + (anchor.into(), merkle_path) + }; + + // Build the Orchard bundle we'll be using. + let mut builder = Builder::new( + MainNetwork, + 10_000_000.into(), + BuildConfig::Standard { + sapling_anchor: Some(anchor), + orchard_anchor: Some(orchard::Anchor::empty_tree()), + }, + ); + builder + .add_sapling_spend::(&sapling_extsk, note, merkle_path) + .unwrap(); + builder + .add_orchard_output::( + Some(sapling_dfvk.to_ovk(zip32::Scope::External).0.into()), + recipient, + 100_000, + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_sapling_output::( + Some(sapling_dfvk.to_ovk(zip32::Scope::Internal)), + sapling_internal_dfvk.find_address(0u32.into()).unwrap().1, + Zatoshis::const_from_u64(880_000), + MemoBytes::empty(), + ) + .unwrap(); + let PcztResult { + pczt_parts, + sapling_meta, + .. + } = builder + .build_for_pczt(OsRng, &zip317::FeeRule::standard()) + .unwrap(); + + // Create the base PCZT. + let pczt = Creator::build_from_parts(pczt_parts).unwrap(); + check_round_trip(&pczt); + + // Finalize the I/O. + let pczt = IoFinalizer::new(pczt).finalize_io().unwrap(); + check_round_trip(&pczt); + + // To test the Combiner, we will create the Sapling proofs, Sapling signatures, and + // Orchard proof "in parallel". + + // Create Sapling proofs. + let sapling_prover = LocalTxProver::bundled(); + let pczt_with_sapling_proofs = Prover::new(pczt.clone()) + .create_sapling_proofs(&sapling_prover, &sapling_prover) + .unwrap() + .finish(); + check_round_trip(&pczt_with_sapling_proofs); + + // Create Orchard proof. + let pczt_with_orchard_proof = Prover::new(pczt.clone()) + .create_orchard_proof(orchard_proving_key()) + .unwrap() + .finish(); + check_round_trip(&pczt_with_orchard_proof); + + // Pass the PCZT to be signed through a serialization cycle to ensure we don't lose + // any information. This emulates passing it to another device. + let pczt = Pczt::parse(&pczt.serialize()).unwrap(); + + // Apply signatures. + let index = sapling_meta.spend_index(0).unwrap(); + let mut signer = Signer::new(pczt).unwrap(); + signer + .sign_sapling(index, &sapling_extsk.expsk.ask) + .unwrap(); + let pczt_with_sapling_signatures = signer.finish(); + check_round_trip(&pczt_with_sapling_signatures); + + // Emulate passing the signed PCZT back to the first device. + let pczt_with_sapling_signatures = + Pczt::parse(&pczt_with_sapling_signatures.serialize()).unwrap(); + + // Combine the three PCZTs into one. + let pczt = Combiner::new(vec![ + pczt_with_sapling_proofs, + pczt_with_orchard_proof, + pczt_with_sapling_signatures, + ]) + .combine() + .unwrap(); + check_round_trip(&pczt); + + // We should now be able to extract the fully authorized transaction. + let (spend_vk, output_vk) = sapling_prover.verifying_keys(); + let tx = TransactionExtractor::new(pczt) + .with_sapling(&spend_vk, &output_vk) + .extract() + .unwrap(); + + assert_eq!(u32::from(tx.expiry_height()), 10_000_040); +} + +#[test] +fn orchard_to_orchard() { + let mut rng = OsRng; + + // Create an Orchard account to receive funds. + let orchard_sk = orchard::keys::SpendingKey::from_bytes([0; 32]).unwrap(); + let orchard_ask = orchard::keys::SpendAuthorizingKey::from(&orchard_sk); + let orchard_fvk = orchard::keys::FullViewingKey::from(&orchard_sk); + let orchard_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::External); + let orchard_ovk = orchard_fvk.to_ovk(orchard::keys::Scope::External); + let recipient = orchard_fvk.address_at(0u32, orchard::keys::Scope::External); + + // Pretend we already received a note. + let value = orchard::value::NoteValue::from_raw(1_000_000); + let note = { + let mut orchard_builder = orchard::builder::Builder::new( + orchard::builder::BundleType::DEFAULT, + orchard::Anchor::empty_tree(), + ); + orchard_builder + .add_output(None, recipient, value, None) + .unwrap(); + let (bundle, meta) = orchard_builder.build::(&mut rng).unwrap().unwrap(); + let action = bundle + .actions() + .get(meta.output_action_index(0).unwrap()) + .unwrap(); + let domain = orchard::note_encryption::OrchardDomain::for_action(action); + let (note, _, _) = try_note_decryption(&domain, &orchard_ivk.prepare(), action).unwrap(); + note + }; + + // Use the tree with a single leaf. + let (anchor, merkle_path) = { + let cmx: orchard::note::ExtractedNoteCommitment = note.commitment().into(); + let leaf = MerkleHashOrchard::from_cmx(&cmx); + let mut tree = + ShardTree::<_, 32, 16>::new(MemoryShardStore::::empty(), 100); + tree.append(leaf, incrementalmerkletree::Retention::Marked) + .unwrap(); + tree.checkpoint(9_999_999).unwrap(); + let position = 0.into(); + let merkle_path = tree + .witness_at_checkpoint_depth(position, 0) + .unwrap() + .unwrap(); + let anchor = merkle_path.root(leaf); + (anchor.into(), merkle_path.into()) + }; + + // Build the Orchard bundle we'll be using. + let mut builder = Builder::new( + MainNetwork, + 10_000_000.into(), + BuildConfig::Standard { + sapling_anchor: None, + orchard_anchor: Some(anchor), + }, + ); + builder + .add_orchard_spend::(&orchard_sk, note, merkle_path) + .unwrap(); + builder + .add_orchard_output::( + Some(orchard_ovk), + recipient, + 100_000, + MemoBytes::empty(), + ) + .unwrap(); + builder + .add_orchard_output::( + Some(orchard_fvk.to_ovk(zip32::Scope::Internal)), + orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal), + 890_000, + MemoBytes::empty(), + ) + .unwrap(); + let PcztResult { + pczt_parts, + orchard_meta, + .. + } = builder + .build_for_pczt(OsRng, &zip317::FeeRule::standard()) + .unwrap(); + + // Create the base PCZT. + let pczt = Creator::build_from_parts(pczt_parts).unwrap(); + check_round_trip(&pczt); + + // Finalize the I/O. + let pczt = IoFinalizer::new(pczt).finalize_io().unwrap(); + check_round_trip(&pczt); + + // Create proofs. + let pczt = Prover::new(pczt) + .create_orchard_proof(orchard_proving_key()) + .unwrap() + .finish(); + check_round_trip(&pczt); + + // Apply signatures. + let index = orchard_meta.spend_action_index(0).unwrap(); + let mut signer = Signer::new(pczt).unwrap(); + signer.sign_orchard(index, &orchard_ask).unwrap(); + let pczt = signer.finish(); + check_round_trip(&pczt); + + // We should now be able to extract the fully authorized transaction. + let tx = TransactionExtractor::new(pczt).extract().unwrap(); + + assert_eq!(u32::from(tx.expiry_height()), 10_000_040); +} diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index ff23245bd8..4187397c7d 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -148,6 +148,15 @@ who = "Daira-Emma Hopwood " criteria = "safe-to-deploy" delta = "0.3.29 -> 0.3.30" +[[audits.getset]] +who = "Jack Grigg " +criteria = "safe-to-deploy" +version = "0.1.3" +notes = """ +Does what it says on the tin. The proc macro generates unsurprising and obvious +code, and does not produce unsafe code or access any imports. +""" + [[audits.h2]] who = "Daira-Emma Hopwood " criteria = "safe-to-deploy" diff --git a/supply-chain/config.toml b/supply-chain/config.toml index da79faef09..76b2887082 100644 --- a/supply-chain/config.toml +++ b/supply-chain/config.toml @@ -150,6 +150,10 @@ criteria = "safe-to-deploy" version = "0.6.0" criteria = "safe-to-deploy" +[[exemptions.atomic-polyfill]] +version = "1.0.3" +criteria = "safe-to-deploy" + [[exemptions.atomic-waker]] version = "1.1.2" criteria = "safe-to-deploy" @@ -286,6 +290,10 @@ criteria = "safe-to-run" version = "0.5.0" criteria = "safe-to-run" +[[exemptions.critical-section]] +version = "1.2.0" +criteria = "safe-to-deploy" + [[exemptions.crossbeam-channel]] version = "0.5.8" criteria = "safe-to-deploy" @@ -446,6 +454,10 @@ criteria = "safe-to-deploy" version = "0.13.8" criteria = "safe-to-deploy" +[[exemptions.embedded-io]] +version = "0.6.1" +criteria = "safe-to-deploy" + [[exemptions.enum-ordinalize]] version = "3.1.15" criteria = "safe-to-deploy" @@ -558,6 +570,10 @@ criteria = "safe-to-deploy" version = "0.3.21" criteria = "safe-to-deploy" +[[exemptions.hash32]] +version = "0.2.1" +criteria = "safe-to-deploy" + [[exemptions.hashbrown]] version = "0.14.2" criteria = "safe-to-deploy" @@ -570,6 +586,10 @@ criteria = "safe-to-deploy" version = "0.7.0" criteria = "safe-to-deploy" +[[exemptions.heapless]] +version = "0.7.17" +criteria = "safe-to-deploy" + [[exemptions.hermit-abi]] version = "0.3.3" criteria = "safe-to-deploy" @@ -886,6 +906,10 @@ criteria = "safe-to-deploy" version = "0.5.0" criteria = "safe-to-deploy" +[[exemptions.postcard]] +version = "1.1.1" +criteria = "safe-to-deploy" + [[exemptions.pprof]] version = "0.13.0" criteria = "safe-to-run" @@ -914,6 +938,14 @@ criteria = "safe-to-deploy" version = "1.2.1" criteria = "safe-to-deploy" +[[exemptions.proc-macro-error-attr2]] +version = "2.0.0" +criteria = "safe-to-deploy" + +[[exemptions.proc-macro-error2]] +version = "2.0.1" +criteria = "safe-to-deploy" + [[exemptions.proptest]] version = "1.3.1" criteria = "safe-to-deploy" @@ -1118,10 +1150,6 @@ criteria = "safe-to-deploy" version = "3.8.1" criteria = "safe-to-deploy" -[[exemptions.sha2]] -version = "0.10.8" -criteria = "safe-to-deploy" - [[exemptions.shellexpand]] version = "3.1.0" criteria = "safe-to-deploy" @@ -1178,6 +1206,10 @@ criteria = "safe-to-deploy" version = "0.6.6" criteria = "safe-to-deploy" +[[exemptions.stable_deref_trait]] +version = "1.2.0" +criteria = "safe-to-deploy" + [[exemptions.str_stack]] version = "0.1.0" criteria = "safe-to-run" diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock index dfe1e95b1d..61008c2d12 100644 --- a/supply-chain/imports.lock +++ b/supply-chain/imports.lock @@ -403,6 +403,12 @@ criteria = "safe-to-deploy" version = "0.4.4" notes = "Most unsafe is hidden by `inout` dependency; only remaining unsafe is raw-splitting a slice and an unreachable hint. Older versions of this regularly reach ~150k daily downloads." +[[audits.bytecode-alliance.audits.cobs]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.2.3" +notes = "No `unsafe` code in the crate and no usage of `std`" + [[audits.bytecode-alliance.audits.constant_time_eq]] who = "Nick Fitzgerald " criteria = "safe-to-deploy" @@ -428,6 +434,12 @@ who = "Benjamin Bouvier " criteria = "safe-to-deploy" delta = "0.9.0 -> 0.10.3" +[[audits.bytecode-alliance.audits.embedded-io]] +who = "Alex Crichton " +criteria = "safe-to-deploy" +version = "0.4.0" +notes = "No `unsafe` code and only uses `std` in ways one would expect the crate to do so." + [[audits.bytecode-alliance.audits.errno]] who = "Dan Gohman " criteria = "safe-to-deploy" @@ -1417,12 +1429,6 @@ version = "0.10.5" notes = "Reviewed on https://fxrev.dev/712371." aggregated-from = "https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/third_party/rust_crates/supply-chain/audits.toml?format=TEXT" -[[audits.google.audits.stable_deref_trait]] -who = "George Burgess IV " -criteria = "safe-to-run" -version = "1.2.0" -aggregated-from = "https://chromium.googlesource.com/chromiumos/third_party/rust_crates/+/refs/heads/main/cargo-vet/audits.toml?format=TEXT" - [[audits.google.audits.static_assertions]] who = "Lukasz Anforowicz " criteria = "safe-to-deploy" @@ -1806,6 +1812,11 @@ who = "Ameer Ghani " criteria = "safe-to-deploy" version = "1.12.1" +[[audits.isrg.audits.sha2]] +who = "David Cook " +criteria = "safe-to-deploy" +version = "0.10.2" + [[audits.isrg.audits.sha3]] who = "David Cook " criteria = "safe-to-deploy" @@ -2340,6 +2351,23 @@ criteria = "safe-to-deploy" delta = "0.6.27 -> 0.6.28" aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" +[[audits.mozilla.audits.sha2]] +who = "Mike Hommey " +criteria = "safe-to-deploy" +delta = "0.10.2 -> 0.10.6" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + +[[audits.mozilla.audits.sha2]] +who = "Jeff Muizelaar " +criteria = "safe-to-deploy" +delta = "0.10.6 -> 0.10.8" +notes = """ +The bulk of this is https://github.com/RustCrypto/hashes/pull/490 which adds aarch64 support along with another PR adding longson. +I didn't check the implementation thoroughly but there wasn't anything obviously nefarious. 0.10.8 has been out for more than a year +which suggests no one else has found anything either. +""" +aggregated-from = "https://hg.mozilla.org/mozilla-central/raw-file/tip/supply-chain/audits.toml" + [[audits.mozilla.audits.strsim]] who = "Ben Dean-Kawamura " criteria = "safe-to-deploy" diff --git a/zcash_primitives/CHANGELOG.md b/zcash_primitives/CHANGELOG.md index 4b8d6a0e59..4d4e2df42b 100644 --- a/zcash_primitives/CHANGELOG.md +++ b/zcash_primitives/CHANGELOG.md @@ -7,6 +7,17 @@ and this library adheres to Rust's notion of ## [Unreleased] +### Added +- `zcash_primitives::transaction` + - `TransactionData::try_map_bundles` + - `builder::{PcztResult, PcztParts}` + - `builder::Builder::build_for_pczt` + - `components::transparent`: + - `pczt` module. + - `EffectsOnly` + - `impl MapAuth for ()` + - `sighash::SighashType` + ## [0.20.0] - 2024-11-14 ### Added diff --git a/zcash_primitives/Cargo.toml b/zcash_primitives/Cargo.toml index d1421e2002..0e27e2921c 100644 --- a/zcash_primitives/Cargo.toml +++ b/zcash_primitives/Cargo.toml @@ -59,12 +59,15 @@ proptest = { workspace = true, optional = true } # - Transparent inputs # - `Error` type exposed -bip32 = { workspace = true, optional = true } +bip32.workspace = true # - `SecretKey` and `PublicKey` types exposed secp256k1 = { workspace = true, optional = true } # Dependencies used internally: # (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.) +# - Boilerplate +getset.workspace = true + # - Documentation document-features.workspace = true @@ -76,8 +79,8 @@ hex.workspace = true # - Shielded protocols redjubjub = "0.7" -# - Transparent inputs -ripemd = { workspace = true, optional = true } +# - Transparent protocol +ripemd.workspace = true # - ZIP 32 aes.workspace = true @@ -108,7 +111,7 @@ default = ["multicore"] multicore = ["orchard/multicore", "sapling/multicore"] ## Enables spending transparent notes with the transaction builder. -transparent-inputs = ["dep:bip32", "dep:ripemd", "dep:secp256k1"] +transparent-inputs = ["bip32/secp256k1-ffi", "dep:secp256k1"] ### A temporary feature flag that exposes granular APIs needed by `zcashd`. These APIs ### should not be relied upon and will be removed in a future release. diff --git a/zcash_primitives/src/transaction/builder.rs b/zcash_primitives/src/transaction/builder.rs index dda20bcec9..855f4c2335 100644 --- a/zcash_primitives/src/transaction/builder.rs +++ b/zcash_primitives/src/transaction/builder.rs @@ -6,6 +6,7 @@ use std::fmt; use std::sync::mpsc::Sender; use rand::{CryptoRng, RngCore}; +use zcash_protocol::consensus::Parameters; use crate::{ consensus::{self, BlockHeight, BranchId, NetworkUpgrade}, @@ -272,6 +273,30 @@ impl BuildResult { } } +/// The result of [`Builder::build_for_pczt`]. +/// +/// It includes the PCZT components along with metadata describing how spends and outputs +/// were shuffled in creating the transaction's shielded bundles. +#[derive(Debug)] +pub struct PcztResult { + pub pczt_parts: PcztParts

, + pub sapling_meta: SaplingMetadata, + pub orchard_meta: orchard::builder::BundleMetadata, +} + +/// The components of a PCZT. +#[derive(Debug)] +pub struct PcztParts { + pub params: P, + pub version: TxVersion, + pub consensus_branch_id: BranchId, + pub lock_time: u32, + pub expiry_height: BlockHeight, + pub transparent: Option, + pub sapling: Option, + pub orchard: Option, +} + /// Generates a [`Transaction`] from its inputs and outputs. pub struct Builder<'a, P, U: sapling::builder::ProverProgress> { params: P, @@ -835,6 +860,86 @@ impl<'a, P: consensus::Parameters, U: sapling::builder::ProverProgress> Builder< orchard_meta, }) } + + /// Builds a PCZT from the configured spends and outputs. + /// + /// Upon success, returns a struct containing the PCZT components, and the + /// [`SaplingMetadata`] and [`orchard::builder::BundleMetadata`] generated during the + /// build process. + pub fn build_for_pczt( + self, + mut rng: R, + fee_rule: &FR, + ) -> Result, Error> { + let fee = self.get_fee(fee_rule).map_err(Error::Fee)?; + let consensus_branch_id = BranchId::for_height(&self.params, self.target_height); + + // determine transaction version + let version = TxVersion::suggested_for_branch(consensus_branch_id); + + let consensus_branch_id = BranchId::for_height(&self.params, self.target_height); + + // + // Consistency checks + // + + // After fees are accounted for, the value balance of the transaction must be zero. + let balance_after_fees = + (self.value_balance()? - fee.into()).ok_or(BalanceError::Underflow)?; + + match balance_after_fees.cmp(&Amount::zero()) { + Ordering::Less => { + return Err(Error::InsufficientFunds(-balance_after_fees)); + } + Ordering::Greater => { + return Err(Error::ChangeRequired(balance_after_fees)); + } + Ordering::Equal => (), + }; + + let transparent_bundle = self.transparent_builder.build_for_pczt(); + + let (sapling_bundle, sapling_meta) = match self + .sapling_builder + .map(|builder| { + builder + .build_for_pczt(&mut rng) + .map_err(Error::SaplingBuild) + }) + .transpose()? + { + Some((bundle, meta)) => (Some(bundle), meta), + None => (None, SaplingMetadata::empty()), + }; + + let (orchard_bundle, orchard_meta) = match self + .orchard_builder + .map(|builder| { + builder + .build_for_pczt(&mut rng) + .map_err(Error::OrchardBuild) + }) + .transpose()? + { + Some((bundle, meta)) => (Some(bundle), meta), + None => (None, orchard::builder::BundleMetadata::empty()), + }; + + Ok(PcztResult { + pczt_parts: PcztParts { + params: self.params, + version, + consensus_branch_id, + lock_time: 0, + expiry_height: self.expiry_height, + transparent: transparent_bundle, + sapling: sapling_bundle, + orchard: orchard_bundle, + }, + sapling_meta, + orchard_meta, + }) + } } #[cfg(zcash_unstable = "zfuture")] diff --git a/zcash_primitives/src/transaction/components/transparent.rs b/zcash_primitives/src/transaction/components/transparent.rs index e2c82fd848..ce441f9780 100644 --- a/zcash_primitives/src/transaction/components/transparent.rs +++ b/zcash_primitives/src/transaction/components/transparent.rs @@ -7,17 +7,42 @@ use std::io::{self, Read, Write}; use crate::{ legacy::{Script, TransparentAddress}, - transaction::TxId, + transaction::{sighash::TransparentAuthorizingContext, TxId}, }; use super::amount::{Amount, BalanceError, NonNegativeAmount}; pub mod builder; +pub mod pczt; pub trait Authorization: Debug { type ScriptSig: Debug + Clone + PartialEq; } +/// Marker type for a bundle that contains no authorizing data, and the necessary input +/// information for creating sighashes. +#[derive(Debug)] +pub struct EffectsOnly { + inputs: Vec, +} + +impl Authorization for EffectsOnly { + type ScriptSig = (); +} + +impl TransparentAuthorizingContext for EffectsOnly { + fn input_amounts(&self) -> Vec { + self.inputs.iter().map(|input| input.value).collect() + } + + fn input_scriptpubkeys(&self) -> Vec