From 7f4db09781d94d42a37dcdbef7c7cfced86ee0e4 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 9 Oct 2024 20:20:10 +0000 Subject: [PATCH 01/18] pczt: Create structure that can be converted into a v5 transaction --- Cargo.lock | 11 + Cargo.toml | 1 + pczt/Cargo.toml | 27 ++ pczt/src/common.rs | 168 +++++++++ pczt/src/lib.rs | 44 +++ pczt/src/orchard.rs | 303 +++++++++++++++++ pczt/src/roles.rs | 28 ++ pczt/src/roles/combiner/mod.rs | 62 ++++ pczt/src/roles/creator/mod.rs | 86 +++++ pczt/src/roles/tx_extractor/mod.rs | 236 +++++++++++++ pczt/src/roles/tx_extractor/orchard.rs | 101 ++++++ pczt/src/roles/tx_extractor/sapling.rs | 110 ++++++ pczt/src/roles/tx_extractor/transparent.rs | 90 +++++ pczt/src/sapling.rs | 318 ++++++++++++++++++ pczt/src/transparent.rs | 199 +++++++++++ zcash_primitives/CHANGELOG.md | 5 + .../src/transaction/components/transparent.rs | 19 ++ zcash_proofs/Cargo.toml | 2 +- 18 files changed, 1809 insertions(+), 1 deletion(-) create mode 100644 pczt/src/common.rs create mode 100644 pczt/src/orchard.rs create mode 100644 pczt/src/roles.rs create mode 100644 pczt/src/roles/combiner/mod.rs create mode 100644 pczt/src/roles/creator/mod.rs create mode 100644 pczt/src/roles/tx_extractor/mod.rs create mode 100644 pczt/src/roles/tx_extractor/orchard.rs create mode 100644 pczt/src/roles/tx_extractor/sapling.rs create mode 100644 pczt/src/roles/tx_extractor/transparent.rs create mode 100644 pczt/src/sapling.rs create mode 100644 pczt/src/transparent.rs diff --git a/Cargo.lock b/Cargo.lock index a0390d68d1..434c037740 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2865,6 +2865,17 @@ dependencies = [ [[package]] name = "pczt" version = "0.0.0" +dependencies = [ + "bls12_381", + "nonempty", + "orchard", + "rand_core", + "redjubjub", + "sapling-crypto", + "zcash_note_encryption", + "zcash_primitives", + "zcash_protocol", +] [[package]] name = "pem-rfc7468" diff --git a/Cargo.toml b/Cargo.toml index ff0e81c5b4..2b58f9e49a 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 diff --git a/pczt/Cargo.toml b/pczt/Cargo.toml index 7570408751..3826dfbcbb 100644 --- a/pczt/Cargo.toml +++ b/pczt/Cargo.toml @@ -11,3 +11,30 @@ 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 } + +rand_core = { workspace = true, optional = true } + +# Payment protocols +# - Sapling +bls12_381 = { 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 } + +[features] +orchard = ["dep:nonempty", "dep:orchard", "dep:zcash_protocol"] +sapling = [ + "dep:bls12_381", + "dep:redjubjub", + "dep:sapling", + "dep:zcash_note_encryption", + "dep:zcash_protocol", +] +transparent = ["dep:zcash_primitives", "dep:zcash_protocol"] +tx-extractor = ["dep:rand_core", "orchard", "sapling", "transparent"] diff --git a/pczt/src/common.rs b/pczt/src/common.rs new file mode 100644 index 0000000000..67b61f570a --- /dev/null +++ b/pczt/src/common.rs @@ -0,0 +1,168 @@ +pub(crate) const FLAG_INPUTS_MODIFIABLE: u8 = 0b0000_0001; +pub(crate) const FLAG_OUTPUTS_MODIFIABLE: u8 = 0b0000_0010; +pub(crate) const FLAG_HAS_SIGHASH_SINGLE: u8 = 0b0000_0100; + +/// Global fields that are relevant to the transaction as a whole. +#[derive(Clone, Debug)] +pub(crate) 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 Inputs Modifiable Flag and indicates whether inputs can be modified. + /// - This is set to `true` by the Creator. + /// - This is checked by the Constructor before adding 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`. + /// - The Combiner merges this bit towards `false`. + /// - Bit 1 is the Outputs Modifiable Flag and indicates whether outputs can be + /// modified. + /// - This is set to `true` by the Creator. + /// - This is checked by the Constructor before adding 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`. + /// - The Combiner merges this bit towards `false`. + /// - Bit 2 is the Has `SIGHASH_SINGLE` flag and indicates whether the transaction has + /// a `SIGHASH_SINGLE` 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-7 must be 0. + pub(crate) tx_modifiable: u8, +} + +impl Global { + pub(crate) fn merge(self, other: Self) -> Option { + let Self { + tx_version, + version_group_id, + consensus_branch_id, + fallback_lock_time, + expiry_height, + coin_type, + tx_modifiable, + } = 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_INPUTS_MODIFIABLE) == 0 { + self.tx_modifiable &= !FLAG_INPUTS_MODIFIABLE; + } + if (tx_modifiable & FLAG_OUTPUTS_MODIFIABLE) == 0 { + self.tx_modifiable &= !FLAG_OUTPUTS_MODIFIABLE; + } + // - Bit 2 merges towards `true`. + if (tx_modifiable & FLAG_HAS_SIGHASH_SINGLE) != 0 { + self.tx_modifiable |= FLAG_HAS_SIGHASH_SINGLE; + } + // - Bits 3-7 must be 0. + if (self.tx_modifiable >> 3) != 0 || (tx_modifiable >> 3) != 0 { + return None; + } + + Some(self) + } +} + +#[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, 0b1000_0000, None), + (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)), + ] { + 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..1a939f2418 100644 --- a/pczt/src/lib.rs +++ b/pczt/src/lib.rs @@ -1 +1,45 @@ +//! 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. +//! - Combiner (anyone can execute) +//! - Combines several PCZTs that represent the same transaction into a single PCZT. +//! - Transaction Extractor (anyone can execute) +//! - Creates bindingSig and extracts the final transaction. +pub mod roles; + +mod common; +mod orchard; +mod sapling; +mod transparent; + +const V5_TX_VERSION: u32 = 5; +const V5_VERSION_GROUP_ID: u32 = 0x26A7270A; + +/// A partially-created Zcash transaction. +#[derive(Clone, Debug)] +pub struct Pczt { + /// Global fields that are relevant to the transaction as a whole. + 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. + // + transparent: transparent::Bundle, + sapling: sapling::Bundle, + orchard: orchard::Bundle, +} diff --git a/pczt/src/orchard.rs b/pczt/src/orchard.rs new file mode 100644 index 0000000000..b078e2cada --- /dev/null +++ b/pczt/src/orchard.rs @@ -0,0 +1,303 @@ +use crate::roles::combiner::merge_optional; + +/// PCZT fields that are specific to producing the transaction's Orchard bundle (if any). +#[derive(Clone, Debug)] +pub(crate) 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. + 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)] +pub(crate) 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], + pub(crate) spend: Spend, + pub(crate) output: Output, +} + +/// Information about a Sapling spend within a transaction. +#[derive(Clone, Debug)] +pub(crate) 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. + // + pub(crate) nullifier: [u8; 32], + pub(crate) rk: [u8; 32], + + /// The spend authorization signature. + /// + /// This is set by the Signer. + pub(crate) spend_auth_sig: Option<[u8; 64]>, +} + +/// Information about an Orchard output within a transaction. +#[derive(Clone, Debug)] +pub(crate) 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, +} + +impl Bundle { + /// Merges this bundle with another. + /// + /// Returns `None` if the bundles have conflicting data. + pub(crate) fn merge(mut self, other: Self) -> 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. If the other bundle has more + // spends or outputs than us, move them over; these cannot conflict by + // construction. + (None, None) => { + if actions.len() > self.actions.len() { + 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; + } + } + } + + 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, + }, + output: + Output { + cmx, + ephemeral_key, + enc_ciphertext, + out_ciphertext, + }, + } = 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) { + return None; + } + } + + Some(self) + } +} + +#[cfg(feature = "orchard")] +impl Bundle { + pub(crate) fn to_tx_data( + &self, + action_auth: F, + bundle_auth: G, + ) -> Result>, E> + where + A: orchard::bundle::Authorization, + E: From, + F: Fn(&Action) -> Result<::SpendAuth, E>, + G: FnOnce(&Self) -> Result, + { + use nonempty::NonEmpty; + use orchard::{ + bundle::Flags, + note::{ExtractedNoteCommitment, Nullifier, TransmittedNoteCiphertext}, + primitives::redpallas, + value::ValueCommitment, + Action, Anchor, Bundle, + }; + use zcash_protocol::value::ZatBalance; + + let actions = self + .actions + .iter() + .map(|action| { + let nf = Nullifier::from_bytes(&action.spend.nullifier) + .into_option() + .ok_or(Error::InvalidNullifier)?; + + let rk = redpallas::VerificationKey::try_from(action.spend.rk) + .map_err(|_| Error::InvalidRandomizedKey)?; + + let cmx = ExtractedNoteCommitment::from_bytes(&action.output.cmx) + .into_option() + .ok_or(Error::InvalidExtractedNoteCommitment)?; + + let encrypted_note = TransmittedNoteCiphertext { + epk_bytes: action.output.ephemeral_key, + enc_ciphertext: action + .output + .enc_ciphertext + .as_slice() + .try_into() + .map_err(|_| Error::InvalidEncCiphertext)?, + out_ciphertext: action + .output + .out_ciphertext + .as_slice() + .try_into() + .map_err(|_| Error::InvalidOutCiphertext)?, + }; + + let cv_net = ValueCommitment::from_bytes(&action.cv_net) + .into_option() + .ok_or(Error::InvalidValueCommitment)?; + + let authorization = action_auth(action)?; + + Ok(Action::from_parts( + nf, + rk, + cmx, + encrypted_note, + cv_net, + authorization, + )) + }) + .collect::>()?; + + Ok(if let Some(actions) = NonEmpty::from_vec(actions) { + let flags = Flags::from_byte(self.flags).ok_or(Error::UnexpectedFlagBitsSet)?; + + let value_balance = + ZatBalance::from_u64(self.value_sum).map_err(|e| Error::InvalidValueBalance(e))?; + + let anchor = Anchor::from_bytes(self.anchor) + .into_option() + .ok_or(Error::InvalidAnchor)?; + + let authorization = bundle_auth(&self)?; + + Some(Bundle::from_parts( + actions, + flags, + value_balance, + anchor, + authorization, + )) + } else { + None + }) + } +} + +#[cfg(feature = "orchard")] +#[derive(Debug)] +pub enum Error { + InvalidAnchor, + InvalidEncCiphertext, + InvalidExtractedNoteCommitment, + InvalidNullifier, + InvalidOutCiphertext, + InvalidRandomizedKey, + InvalidValueBalance(zcash_protocol::value::BalanceError), + InvalidValueCommitment, + UnexpectedFlagBitsSet, +} diff --git a/pczt/src/roles.rs b/pczt/src/roles.rs new file mode 100644 index 0000000000..20bf6f1e03 --- /dev/null +++ b/pczt/src/roles.rs @@ -0,0 +1,28 @@ +pub mod creator; + +pub mod combiner; + +#[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 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::MissingBsk), + )); + } +} diff --git a/pczt/src/roles/combiner/mod.rs b/pczt/src/roles/combiner/mod.rs new file mode 100644 index 0000000000..9a2cadb17b --- /dev/null +++ b/pczt/src/roles/combiner/mod.rs @@ -0,0 +1,62 @@ +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 { + Ok(Pczt { + global: lhs.global.merge(rhs.global).ok_or(Error::DataMismatch)?, + transparent: lhs + .transparent + .merge(rhs.transparent) + .ok_or(Error::DataMismatch)?, + sapling: lhs.sapling.merge(rhs.sapling).ok_or(Error::DataMismatch)?, + orchard: lhs.orchard.merge(rhs.orchard).ok_or(Error::DataMismatch)?, + }) +} + +/// 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 +} + +/// 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..8132c5a45b --- /dev/null +++ b/pczt/src/roles/creator/mod.rs @@ -0,0 +1,86 @@ +use crate::{ + common::{FLAG_INPUTS_MODIFIABLE, FLAG_OUTPUTS_MODIFIABLE}, + Pczt, V5_TX_VERSION, V5_VERSION_GROUP_ID, +}; + +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, + // Spends and outputs modifiable, no SIGHASH_SINGLE. + tx_modifiable: FLAG_INPUTS_MODIFIABLE | FLAG_OUTPUTS_MODIFIABLE, + }, + 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, + }, + } + } +} diff --git a/pczt/src/roles/tx_extractor/mod.rs b/pczt/src/roles/tx_extractor/mod.rs new file mode 100644 index 0000000000..abf084ed49 --- /dev/null +++ b/pczt/src/roles/tx_extractor/mod.rs @@ -0,0 +1,236 @@ +use std::marker::PhantomData; + +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.map_authorization( + transparent::RemoveInputInfo, + sapling::AddBindingSig { + sighash: shielded_sighash.as_ref(), + }, + orchard::AddBindingSig { + sighash: shielded_sighash.as_ref(), + }, + ); + + 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 = transparent::Unbound; + type SaplingAuth = sapling::Unbound; + type OrchardAuth = orchard::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, + 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 + } +} diff --git a/pczt/src/roles/tx_extractor/orchard.rs b/pczt/src/roles/tx_extractor/orchard.rs new file mode 100644 index 0000000000..f588e53dd5 --- /dev/null +++ b/pczt/src/roles/tx_extractor/orchard.rs @@ -0,0 +1,101 @@ +use orchard::{ + bundle::{Authorization, Authorized}, + circuit::VerifyingKey, + primitives::redpallas, + Bundle, Proof, +}; +use rand_core::OsRng; +use zcash_primitives::transaction::components::orchard::MapAuth; +use zcash_protocol::value::ZatBalance; + +pub(super) fn extract_bundle( + bundle: crate::orchard::Bundle, +) -> Result>, OrchardError> { + bundle.to_tx_data( + |action| { + Ok(redpallas::Signature::from( + action + .spend + .spend_auth_sig + .ok_or(OrchardError::MissingSpendAuthSig)?, + )) + }, + |bundle| { + let proof = Proof::new(bundle.zkproof.clone().ok_or(OrchardError::MissingProof)?); + + let bsk = redpallas::SigningKey::try_from(bundle.bsk.ok_or(OrchardError::MissingBsk)?) + .map_err(|_| OrchardError::InvalidBsk)?; + + Ok(Unbound { proof, bsk }) + }, + ) +} + +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(super) struct Unbound { + proof: Proof, + bsk: redpallas::SigningKey, +} + +impl Authorization for Unbound { + type SpendAuth = redpallas::Signature; +} + +pub(super) struct AddBindingSig<'a> { + pub(super) sighash: &'a [u8; 32], +} + +impl<'a> MapAuth for AddBindingSig<'a> { + fn map_spend_auth( + &self, + s: ::SpendAuth, + ) -> ::SpendAuth { + s + } + + fn map_authorization(&self, a: Unbound) -> Authorized { + Authorized::from_parts(a.proof, a.bsk.sign(OsRng, self.sighash)) + } +} + +#[derive(Debug)] +pub enum OrchardError { + Data(crate::orchard::Error), + InvalidBsk, + InvalidProof, + MissingBsk, + MissingProof, + MissingSpendAuthSig, +} + +impl From for OrchardError { + fn from(e: crate::orchard::Error) -> Self { + Self::Data(e) + } +} diff --git a/pczt/src/roles/tx_extractor/sapling.rs b/pczt/src/roles/tx_extractor/sapling.rs new file mode 100644 index 0000000000..f0939a76b4 --- /dev/null +++ b/pczt/src/roles/tx_extractor/sapling.rs @@ -0,0 +1,110 @@ +use rand_core::OsRng; +use sapling::{ + bundle::{Authorization, Authorized}, + circuit::{OutputVerifyingKey, SpendVerifyingKey}, + BatchValidator, Bundle, +}; +use zcash_primitives::transaction::components::{sapling::MapAuth, GROTH_PROOF_SIZE}; +use zcash_protocol::value::ZatBalance; + +pub(super) fn extract_bundle( + bundle: crate::sapling::Bundle, +) -> Result>, SaplingError> { + bundle.to_tx_data( + |spend| spend.zkproof.ok_or(SaplingError::MissingProof), + |spend| { + Ok(redjubjub::Signature::from( + spend + .spend_auth_sig + .ok_or(SaplingError::MissingSpendAuthSig)?, + )) + }, + |output| output.zkproof.ok_or(SaplingError::MissingProof), + |bundle| { + let bsk = redjubjub::SigningKey::try_from(bundle.bsk.ok_or(SaplingError::MissingBsk)?) + .map_err(|_| SaplingError::InvalidBsk)?; + + Ok(Unbound { bsk }) + }, + ) +} + +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(super) struct Unbound { + bsk: redjubjub::SigningKey, +} + +impl Authorization for Unbound { + type SpendProof = [u8; GROTH_PROOF_SIZE]; + type OutputProof = [u8; GROTH_PROOF_SIZE]; + type AuthSig = redjubjub::Signature; +} + +pub(super) struct AddBindingSig<'a> { + pub(super) sighash: &'a [u8; 32], +} + +impl<'a> MapAuth for AddBindingSig<'a> { + fn map_spend_proof( + &mut self, + p: ::SpendProof, + ) -> ::SpendProof { + p + } + + fn map_output_proof( + &mut self, + p: ::OutputProof, + ) -> ::OutputProof { + p + } + + fn map_auth_sig( + &mut self, + s: ::AuthSig, + ) -> ::AuthSig { + s + } + + fn map_authorization(&mut self, a: Unbound) -> Authorized { + Authorized { + binding_sig: a.bsk.sign(OsRng, self.sighash), + } + } +} + +#[derive(Debug)] +pub enum SaplingError { + ConsensusRuleViolation, + Data(crate::sapling::Error), + InvalidBsk, + InvalidProofsOrSignatures, + MissingBsk, + MissingProof, + MissingSpendAuthSig, +} + +impl From for SaplingError { + fn from(e: crate::sapling::Error) -> Self { + Self::Data(e) + } +} diff --git a/pczt/src/roles/tx_extractor/transparent.rs b/pczt/src/roles/tx_extractor/transparent.rs new file mode 100644 index 0000000000..218d433701 --- /dev/null +++ b/pczt/src/roles/tx_extractor/transparent.rs @@ -0,0 +1,90 @@ +use zcash_primitives::{ + legacy::Script, + transaction::{ + components::transparent::{Authorization, Authorized, Bundle, MapAuth, TxOut}, + sighash::TransparentAuthorizingContext, + }, +}; +use zcash_protocol::value::Zatoshis; + +pub(super) fn extract_bundle( + bundle: crate::transparent::Bundle, +) -> Result>, TransparentError> { + bundle.to_tx_data( + |input| { + Ok(Script( + input + .script_sig + .clone() + .ok_or(TransparentError::MissingScriptSig)?, + )) + }, + |bundle| { + let inputs = bundle + .inputs + .iter() + .map(|input| { + let value = Zatoshis::from_u64(input.value) + .map_err(|_| crate::transparent::Error::InvalidValue)?; + let script_pubkey = Script(input.script_pubkey.clone()); + + Ok(TxOut { + value, + script_pubkey, + }) + }) + .collect::>()?; + + Ok(Unbound { inputs }) + }, + ) +} + +#[derive(Debug)] +pub(super) struct Unbound { + inputs: Vec, +} + +impl Authorization for Unbound { + type ScriptSig = Script; +} + +impl TransparentAuthorizingContext for Unbound { + fn input_amounts(&self) -> Vec { + self.inputs.iter().map(|input| input.value).collect() + } + + fn input_scriptpubkeys(&self) -> Vec