diff --git a/CHANGELOG.md b/CHANGELOG.md index 0232412f6..55ab296f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to Rust's notion of ## [Unreleased] +### Added +- Support for Partially-Created Zcash Transactions: + - `orchard::builder::Builder::build_for_pczt` + - `orchard::pczt` module. +- `orchard::bundle::EffectsOnly` +- `orchard::tree::MerklePath::{position, auth_path}` +- `orchard::value`: + - `Sign` + - `ValueSum::magnitude_sign` + - `ValueCommitTrapdoor::to_bytes` +- `impl Clone for orchard::tree::MerklePath` + ## [0.10.0] - 2024-10-02 ### Changed diff --git a/Cargo.lock b/Cargo.lock index fa7c6bf1f..5482a493e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -908,6 +908,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.31", +] + [[package]] name = "gif" version = "0.11.4" @@ -1423,6 +1435,7 @@ dependencies = [ "criterion", "ff", "fpe", + "getset", "group", "halo2_gadgets", "halo2_proofs", @@ -1652,6 +1665,28 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[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.31", +] + [[package]] name = "proc-macro2" version = "1.0.66" @@ -2032,9 +2067,9 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symbolic-common" diff --git a/Cargo.toml b/Cargo.toml index 012433bb2..8c014ecdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,9 @@ zcash_spec = "0.1" zip32 = "0.1" visibility = "0.1.1" +# Boilerplate +getset = "0.1" + # Logging tracing = "0.1" diff --git a/src/builder.rs b/src/builder.rs index 37bdc3883..0b6f05b60 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2,6 +2,7 @@ use core::fmt; use core::iter; +use std::collections::BTreeMap; use std::fmt::Display; use ff::Field; @@ -18,7 +19,7 @@ use crate::{ FullViewingKey, OutgoingViewingKey, Scope, SpendAuthorizingKey, SpendValidatingKey, SpendingKey, }, - note::{Note, Rho, TransmittedNoteCiphertext}, + note::{ExtractedNoteCommitment, Note, Nullifier, Rho, TransmittedNoteCiphertext}, note_encryption::OrchardNoteEncryption, primitives::redpallas::{self, Binding, SpendAuth}, tree::{Anchor, MerklePath}, @@ -261,6 +262,48 @@ impl SpendInfo { &path_root == anchor } } + + /// Builds the spend half of an action. + /// + /// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend]. + /// + /// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend + fn build( + &self, + mut rng: impl RngCore, + ) -> ( + Nullifier, + SpendValidatingKey, + pallas::Scalar, + redpallas::VerificationKey, + ) { + let nf_old = self.note.nullifier(&self.fvk); + let ak: SpendValidatingKey = self.fvk.clone().into(); + let alpha = pallas::Scalar::random(&mut rng); + let rk = ak.randomize(&alpha); + + (nf_old, ak, alpha, rk) + } + + fn into_pczt(self, rng: impl RngCore) -> crate::pczt::Spend { + let (nf_old, _, alpha, rk) = self.build(rng); + + crate::pczt::Spend { + nullifier: nf_old, + rk, + spend_auth_sig: None, + recipient: Some(self.note.recipient()), + value: Some(self.note.value()), + rho: Some(self.note.rho()), + rseed: Some(self.note.rseed().clone()), + fvk: Some(self.fvk), + witness: Some(self.merkle_path), + alpha: Some(alpha), + zip32_derivation: None, + dummy_sk: self.dummy_sk, + proprietary: BTreeMap::new(), + } + } } /// Information about a specific output to receive funds in an [`Action`]. @@ -301,6 +344,54 @@ impl OutputInfo { Self::new(None, recipient, NoteValue::zero(), None) } + + /// Builds the output half of an action. + /// + /// Defined in [Zcash Protocol Spec § 4.7.3: Sending Notes (Orchard)][orchardsend]. + /// + /// [orchardsend]: https://zips.z.cash/protocol/nu5.pdf#orchardsend + fn build( + &self, + cv_net: &ValueCommitment, + nf_old: Nullifier, + mut rng: impl RngCore, + ) -> (Note, ExtractedNoteCommitment, TransmittedNoteCiphertext) { + let rho = Rho::from_nf_old(nf_old); + let note = Note::new(self.recipient, self.value, rho, &mut rng); + let cm_new = note.commitment(); + let cmx = cm_new.into(); + + let encryptor = OrchardNoteEncryption::new(self.ovk.clone(), note, self.memo); + + let encrypted_note = TransmittedNoteCiphertext { + epk_bytes: encryptor.epk().to_bytes().0, + enc_ciphertext: encryptor.encrypt_note_plaintext(), + out_ciphertext: encryptor.encrypt_outgoing_plaintext(cv_net, &cmx, &mut rng), + }; + + (note, cmx, encrypted_note) + } + + fn into_pczt( + self, + cv_net: &ValueCommitment, + nf_old: Nullifier, + rng: impl RngCore, + ) -> crate::pczt::Output { + let (note, cmx, encrypted_note) = self.build(cv_net, nf_old, rng); + + crate::pczt::Output { + cmx, + encrypted_note, + recipient: Some(self.recipient), + value: Some(self.value), + rseed: Some(note.rseed().clone()), + // TODO: Save this? + ock: None, + zip32_derivation: None, + proprietary: BTreeMap::new(), + } + } } /// Information about a specific [`Action`] we plan to build. @@ -334,23 +425,8 @@ impl ActionInfo { let v_net = self.value_sum(); let cv_net = ValueCommitment::derive(v_net, self.rcv.clone()); - let nf_old = self.spend.note.nullifier(&self.spend.fvk); - let rho = Rho::from_nf_old(nf_old); - let ak: SpendValidatingKey = self.spend.fvk.clone().into(); - let alpha = pallas::Scalar::random(&mut rng); - let rk = ak.randomize(&alpha); - - let note = Note::new(self.output.recipient, self.output.value, rho, &mut rng); - let cm_new = note.commitment(); - let cmx = cm_new.into(); - - let encryptor = OrchardNoteEncryption::new(self.output.ovk, note, self.output.memo); - - let encrypted_note = TransmittedNoteCiphertext { - epk_bytes: encryptor.epk().to_bytes().0, - enc_ciphertext: encryptor.encrypt_note_plaintext(), - out_ciphertext: encryptor.encrypt_outgoing_plaintext(&cv_net, &cmx, &mut rng), - }; + let (nf_old, ak, alpha, rk) = self.spend.build(&mut rng); + let (note, cmx, encrypted_note) = self.output.build(&cv_net, nf_old, &mut rng); ( Action::from_parts( @@ -367,6 +443,21 @@ impl ActionInfo { Circuit::from_action_context_unchecked(self.spend, note, alpha, self.rcv), ) } + + fn build_for_pczt(self, mut rng: impl RngCore) -> crate::pczt::Action { + let v_net = self.value_sum(); + let cv_net = ValueCommitment::derive(v_net, self.rcv.clone()); + + let spend = self.spend.into_pczt(&mut rng); + let output = self.output.into_pczt(&cv_net, spend.nullifier, &mut rng); + + crate::pczt::Action { + cv_net, + spend, + output, + rcv: Some(self.rcv), + } + } } /// Type alias for an in-progress bundle that has no proofs or signatures. @@ -552,6 +643,40 @@ impl Builder { self.outputs, ) } + + /// Builds a bundle containing the given spent notes and outputs along with their + /// metadata, for inclusion in a PCZT. + pub fn build_for_pczt( + self, + rng: impl RngCore, + ) -> Result<(crate::pczt::Bundle, BundleMetadata), BuildError> { + build_bundle( + rng, + self.anchor, + self.bundle_type, + self.spends, + self.outputs, + |pre_actions, flags, value_sum, bundle_meta, mut rng| { + // Create the actions. + let actions = pre_actions + .into_iter() + .map(|a| a.build_for_pczt(&mut rng)) + .collect::>(); + + Ok(( + crate::pczt::Bundle { + actions, + flags, + value_sum, + anchor: self.anchor, + zkproof: None, + bsk: None, + }, + bundle_meta, + )) + }, + ) + } } /// Builds a bundle containing the given spent notes and outputs. @@ -559,12 +684,69 @@ impl Builder { /// The returned bundle will have no proof or signatures; these can be applied with /// [`Bundle::create_proof`] and [`Bundle::apply_signatures`] respectively. pub fn bundle>( - mut rng: impl RngCore, + rng: impl RngCore, anchor: Anchor, bundle_type: BundleType, spends: Vec, outputs: Vec, ) -> Result, BundleMetadata)>, BuildError> { + build_bundle( + rng, + anchor, + bundle_type, + spends, + outputs, + |pre_actions, flags, value_balance, bundle_meta, mut rng| { + let result_value_balance: V = i64::try_from(value_balance) + .map_err(BuildError::ValueSum) + .and_then(|i| { + V::try_from(i).map_err(|_| BuildError::ValueSum(value::OverflowError)) + })?; + + // Compute the transaction binding signing key. + let bsk = pre_actions + .iter() + .map(|a| &a.rcv) + .sum::() + .into_bsk(); + + // Create the actions. + let (actions, circuits): (Vec<_>, Vec<_>) = + pre_actions.into_iter().map(|a| a.build(&mut rng)).unzip(); + + // Verify that bsk and bvk are consistent. + let bvk = (actions.iter().map(|a| a.cv_net()).sum::() + - ValueCommitment::derive(value_balance, ValueCommitTrapdoor::zero())) + .into_bvk(); + assert_eq!(redpallas::VerificationKey::from(&bsk), bvk); + + Ok(NonEmpty::from_vec(actions).map(|actions| { + ( + Bundle::from_parts( + actions, + flags, + result_value_balance, + anchor, + InProgress { + proof: Unproven { circuits }, + sigs: Unauthorized { bsk }, + }, + ), + bundle_meta, + ) + })) + }, + ) +} + +fn build_bundle( + mut rng: R, + anchor: Anchor, + bundle_type: BundleType, + spends: Vec, + outputs: Vec, + finisher: impl FnOnce(Vec, Flags, ValueSum, BundleMetadata, R) -> Result, +) -> Result { let flags = bundle_type.flags(); let num_requested_spends = spends.len(); @@ -640,42 +822,7 @@ pub fn bundle>( }) .ok_or(OverflowError)?; - let result_value_balance: V = i64::try_from(value_balance) - .map_err(BuildError::ValueSum) - .and_then(|i| V::try_from(i).map_err(|_| BuildError::ValueSum(value::OverflowError)))?; - - // Compute the transaction binding signing key. - let bsk = pre_actions - .iter() - .map(|a| &a.rcv) - .sum::() - .into_bsk(); - - // Create the actions. - let (actions, circuits): (Vec<_>, Vec<_>) = - pre_actions.into_iter().map(|a| a.build(&mut rng)).unzip(); - - // Verify that bsk and bvk are consistent. - let bvk = (actions.iter().map(|a| a.cv_net()).sum::() - - ValueCommitment::derive(value_balance, ValueCommitTrapdoor::zero())) - .into_bvk(); - assert_eq!(redpallas::VerificationKey::from(&bsk), bvk); - - Ok(NonEmpty::from_vec(actions).map(|actions| { - ( - Bundle::from_parts( - actions, - flags, - result_value_balance, - anchor, - InProgress { - proof: Unproven { circuits }, - sigs: Unauthorized { bsk }, - }, - ), - bundle_meta, - ) - })) + finisher(pre_actions, flags, value_balance, bundle_meta, rng) } /// Marker trait representing bundle signatures in the process of being created. diff --git a/src/bundle.rs b/src/bundle.rs index 02cd697eb..e2bc9a356 100644 --- a/src/bundle.rs +++ b/src/bundle.rs @@ -409,6 +409,14 @@ impl> Bundle { } } +/// Marker type for a bundle that contains no authorizing data. +#[derive(Clone, Debug)] +pub struct EffectsOnly; + +impl Authorization for EffectsOnly { + type SpendAuth = (); +} + /// Authorizing data for a bundle of actions, ready to be committed to the ledger. #[derive(Debug, Clone)] pub struct Authorized { @@ -519,17 +527,11 @@ pub mod testing { Anchor, }; - use super::{Action, Authorization, Authorized, Bundle, Flags}; + use super::{Action, Authorized, Bundle, Flags}; pub use crate::action::testing::{arb_action, arb_unauthorized_action}; - /// Marker for an unauthorized bundle with no proofs or signatures. - #[derive(Debug)] - pub struct Unauthorized; - - impl Authorization for Unauthorized { - type SpendAuth = (); - } + type Unauthorized = super::EffectsOnly; /// Generate an unauthorized action having spend and output values less than MAX_NOTE_VALUE / n_actions. pub fn arb_unauthorized_action_n( @@ -617,7 +619,7 @@ pub mod testing { flags, balances.into_iter().sum::>().unwrap(), anchor, - Unauthorized + super::EffectsOnly, ) } } diff --git a/src/lib.rs b/src/lib.rs index 005253e97..54db6d4d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ mod constants; pub mod keys; pub mod note; pub mod note_encryption; +pub mod pczt; pub mod primitives; mod spec; pub mod tree; diff --git a/src/pczt.rs b/src/pczt.rs new file mode 100644 index 000000000..44d9322a3 --- /dev/null +++ b/src/pczt.rs @@ -0,0 +1,434 @@ +//! PCZT support for Orchard. + +use std::collections::BTreeMap; +use std::fmt; + +use getset::Getters; +use pasta_curves::pallas; +use zcash_note_encryption::OutgoingCipherKey; +use zip32::ChildIndex; + +use crate::{ + bundle::Flags, + keys::{FullViewingKey, SpendingKey}, + note::{ExtractedNoteCommitment, Nullifier, RandomSeed, Rho, TransmittedNoteCiphertext}, + primitives::redpallas::{self, Binding, SpendAuth}, + tree::MerklePath, + value::{NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum}, + Address, Anchor, Proof, +}; + +mod parse; +pub use parse::ParseError; + +mod io_finalizer; +pub use io_finalizer::IoFinalizerError; + +mod prover; +pub use prover::ProverError; + +mod signer; +pub use signer::SignerError; + +mod tx_extractor; +pub use tx_extractor::{TxExtractorError, Unbound}; + +/// PCZT fields that are specific to producing the transaction's Orchard bundle (if any). +/// +/// This struct is for representing Orchard in a partially-created transaction. If you +/// have a fully-created transaction, use [the regular `Bundle` struct]. +/// +/// [the regular `Bundle` struct]: crate::Bundle +#[derive(Debug, Getters)] +#[getset(get = "pub")] +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. + pub(crate) actions: Vec, + + /// The flags for the Orchard bundle. + /// + /// 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: Flags, + + /// The sum of the values of all `actions`. + /// + /// 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: ValueSum, + + /// The Orchard anchor for this transaction. + /// + /// Set by the Creator. + pub(crate) anchor: Anchor, + + /// 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>, +} + +impl Bundle { + /// Returns a mutable reference to the actions in this bundle. + /// + /// This is used by Signers to apply signatures with [`Action::sign`]. + pub fn actions_mut(&mut self) -> &mut [Action] { + &mut self.actions + } +} + +/// PCZT fields that are specific to producing an Orchard action within a transaction. +/// +/// This struct is for representing Orchard actions in a partially-created transaction. +/// If you have a fully-created transaction, use [the regular `Action` struct]. +/// +/// [the regular `Action` struct]: crate::Action +#[derive(Debug, Getters)] +#[getset(get = "pub")] +pub struct Action { + /// A commitment to the net value created or consumed by this action. + pub(crate) cv_net: ValueCommitment, + + /// The spend half of this action. + pub(crate) spend: Spend, + + /// The output half of this action. + 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, +} + +/// Information about an Orchars spend within a transaction. +#[derive(Debug, Getters)] +#[getset(get = "pub")] +pub struct Spend { + /// The nullifier of the note being spent. + pub(crate) nullifier: Nullifier, + + /// The randomized verification key for the note being spent. + pub(crate) rk: redpallas::VerificationKey, + + /// The spend authorization signature. + /// + /// This is set by the Signer. + pub(crate) spend_auth_sig: Option>, + + /// The address that received the note being spent. + /// + /// - This is set by the Constructor (or Updater?). + /// - This is required by the Prover. + pub(crate) recipient: Option
, + + /// 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. + // + // TODO: This could be merged with `rseed` into a tuple. `recipient` and `value` are + // separate because they might need to be independently redacted. (For which role?) + pub(crate) rho: Option, + + /// The seed randomness for the note being spent. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + pub(crate) rseed: Option, + + /// The full viewing key that received the note being spent. + /// + /// - This is set by the Updater. + /// - This is required by the Prover. + pub(crate) fvk: Option, + + /// 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, + + /// 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, + + /// 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, + + /// Proprietary fields related to the note being spent. + pub(crate) proprietary: BTreeMap>, +} + +/// Information about an Orchard output within a transaction. +#[derive(Getters)] +#[getset(get = "pub")] +pub struct Output { + /// A commitment to the new note being created. + pub(crate) cmx: ExtractedNoteCommitment, + + /// The transmitted note ciphertext. + /// + /// This contains the following PCZT fields: + /// - `ephemeral_key` + /// - `enc_ciphertext` + /// - `out_ciphertext` + pub(crate) encrypted_note: TransmittedNoteCiphertext, + + /// The address that will receive the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// - The Signer can use `recipient` and `rseed` (if present) to verify that + /// `enc_ciphertext` is correctly encrypted (and contains a note plaintext matching + /// the public commitments), and to confirm the value of the memo. + pub(crate) recipient: Option
, + + /// 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. + pub(crate) value: Option, + + /// The seed randomness for the output. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + /// - The Signer can use `recipient` and `rseed` (if present) to verify that + /// `enc_ciphertext` is correctly encrypted (and contains a note plaintext matching + /// the public commitments), and to confirm the value of the memo. + pub(crate) rseed: Option, + + /// 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, + + /// 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. + pub(crate) proprietary: BTreeMap>, +} + +impl fmt::Debug for Output { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Output") + .field("cmx", &self.cmx) + .field("encrypted_note", &self.encrypted_note) + .field("recipient", &self.recipient) + .field("value", &self.value) + .field("rseed", &self.rseed) + .field("zip32_derivation", &self.zip32_derivation) + .field("proprietary", &self.proprietary) + .finish_non_exhaustive() + } +} + +/// The ZIP 32 derivation path at which a key can be found. +#[derive(Debug, Getters, PartialEq)] +#[getset(get = "pub")] +pub struct Zip32Derivation { + /// The [ZIP 32 seed fingerprint](https://zips.z.cash/zip-0032#seed-fingerprints). + seed_fingerprint: [u8; 32], + + /// The sequence of indices corresponding to the shielded HD path. + derivation_path: Vec, +} + +#[cfg(test)] +mod tests { + use bridgetree::BridgeTree; + use ff::{Field, PrimeField}; + use pasta_curves::pallas; + use rand::rngs::OsRng; + + use crate::{ + builder::{Builder, BundleType}, + circuit::ProvingKey, + constants::MERKLE_DEPTH_ORCHARD, + keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey}, + note::{ExtractedNoteCommitment, RandomSeed, Rho}, + pczt::Zip32Derivation, + tree::{MerkleHashOrchard, MerklePath, EMPTY_ROOTS}, + value::NoteValue, + Note, + }; + + #[test] + fn shielding_bundle() { + let pk = ProvingKey::build(); + let mut rng = OsRng; + + let sk = SpendingKey::random(&mut rng); + let fvk = FullViewingKey::from(&sk); + let recipient = fvk.address_at(0u32, Scope::External); + + // Run the Creator and Constructor roles. + let mut builder = Builder::new( + BundleType::DEFAULT, + EMPTY_ROOTS[MERKLE_DEPTH_ORCHARD].into(), + ); + builder + .add_output(None, recipient, NoteValue::from_raw(5000), None) + .unwrap(); + let balance: i64 = builder.value_balance().unwrap(); + assert_eq!(balance, -5000); + let mut pczt_bundle = builder.build_for_pczt(&mut rng).unwrap().0; + + // Run the IO Finalizer role. + let sighash = [0; 32]; + pczt_bundle.finalize_io(sighash, &mut rng).unwrap(); + + // Run the Prover role. + pczt_bundle.create_proof(&pk, &mut rng).unwrap(); + + // Run the Transaction Extractor role. + let bundle = pczt_bundle.extract::().unwrap().unwrap(); + + assert_eq!(bundle.value_balance(), &(-5000)); + // We can successfully bind the bundle. + bundle.apply_binding_signature(sighash, &mut rng).unwrap(); + } + + #[test] + fn shielded_bundle() { + let pk = ProvingKey::build(); + let mut rng = OsRng; + + // Pretend we derived the spending key via ZIP 32. + let zip32_derivation = Zip32Derivation::parse([1; 32], vec![]).unwrap(); + let sk = SpendingKey::random(&mut rng); + let ask = SpendAuthorizingKey::from(&sk); + let fvk = FullViewingKey::from(&sk); + let recipient = fvk.address_at(0u32, Scope::External); + + // Pretend we already received a note. + let value = NoteValue::from_raw(15_000); + let note = { + let rho = Rho::from_bytes(&pallas::Base::random(&mut rng).to_repr()).unwrap(); + loop { + if let Some(note) = + Note::from_parts(recipient, value, rho, RandomSeed::random(&mut rng, &rho)) + .into_option() + { + break note; + } + } + }; + + // Use the tree with a single leaf. + let (anchor, merkle_path) = { + let cmx: ExtractedNoteCommitment = note.commitment().into(); + let leaf = MerkleHashOrchard::from_cmx(&cmx); + let mut tree = BridgeTree::::new(100); + tree.append(leaf); + let position = tree.mark().unwrap(); + let root = tree.root(0).unwrap(); + let auth_path = tree.witness(position, 0).unwrap(); + let merkle_path = MerklePath::from_parts( + u64::from(position).try_into().unwrap(), + auth_path[..].try_into().unwrap(), + ); + let anchor = root.into(); + assert_eq!(anchor, merkle_path.root(cmx)); + (anchor, merkle_path) + }; + + // Run the Creator and Constructor roles. + let mut builder = Builder::new(BundleType::DEFAULT, anchor); + builder.add_spend(fvk.clone(), note, merkle_path).unwrap(); + builder + .add_output(None, recipient, NoteValue::from_raw(10_000), None) + .unwrap(); + builder + .add_output( + Some(fvk.to_ovk(Scope::Internal)), + fvk.address_at(0u32, Scope::Internal), + NoteValue::from_raw(5_000), + None, + ) + .unwrap(); + let balance: i64 = builder.value_balance().unwrap(); + assert_eq!(balance, 0); + let mut pczt_bundle = builder.build_for_pczt(&mut rng).unwrap().0; + + // Run the IO Finalizer role. + let sighash = [0; 32]; + pczt_bundle.finalize_io(sighash, &mut rng).unwrap(); + + // Run the Updater role. + for action in pczt_bundle.actions_mut() { + if action.spend.value() == &Some(value) { + action.spend.zip32_derivation = Some(Zip32Derivation { + seed_fingerprint: zip32_derivation.seed_fingerprint, + derivation_path: zip32_derivation.derivation_path.clone(), + }); + } + } + + // Run the Prover role. + pczt_bundle.create_proof(&pk, &mut rng).unwrap(); + + // TODO: Verify that the PCZT contains sufficient information to decrypt and check + // `enc_ciphertext`. + + // Run the Signer role. + for action in pczt_bundle.actions_mut() { + if action.spend.zip32_derivation.as_ref() == Some(&zip32_derivation) { + action.sign(sighash, &ask, &mut rng).unwrap(); + } + } + + // Run the Transaction Extractor role. + let bundle = pczt_bundle.extract::().unwrap().unwrap(); + + assert_eq!(bundle.value_balance(), &0); + // We can successfully bind the bundle. + bundle.apply_binding_signature(sighash, &mut rng).unwrap(); + } +} diff --git a/src/pczt/io_finalizer.rs b/src/pczt/io_finalizer.rs new file mode 100644 index 000000000..b227df3be --- /dev/null +++ b/src/pczt/io_finalizer.rs @@ -0,0 +1,69 @@ +use rand::{CryptoRng, RngCore}; + +use crate::{ + keys::SpendAuthorizingKey, + primitives::redpallas, + value::{ValueCommitTrapdoor, ValueCommitment}, +}; + +use super::SignerError; + +impl super::Bundle { + /// Finalizes the IO for this bundle. + pub fn finalize_io( + &mut self, + sighash: [u8; 32], + mut rng: R, + ) -> Result<(), IoFinalizerError> { + // Compute the transaction binding signing key. + let rcvs = self + .actions + .iter() + .map(|a| { + a.rcv + .as_ref() + .ok_or(IoFinalizerError::MissingValueCommitTrapdoor) + }) + .collect::, _>>()?; + let bsk = rcvs.into_iter().sum::().into_bsk(); + + // Verify that bsk and bvk are consistent. + let bvk = (self + .actions + .iter() + .map(|a| a.cv_net()) + .sum::() + - ValueCommitment::derive(self.value_sum, ValueCommitTrapdoor::zero())) + .into_bvk(); + if redpallas::VerificationKey::from(&bsk) != bvk { + return Err(IoFinalizerError::ValueCommitMismatch); + } + self.bsk = Some(bsk); + + // Add signatures to dummy spends. + for action in self.actions.iter_mut() { + // The `Option::take` ensures we don't have any spend authorizing keys in the + // PCZT after the IO Finalizer has run. + if let Some(sk) = action.spend.dummy_sk.take() { + let ask = SpendAuthorizingKey::from(&sk); + action + .sign(sighash, &ask, &mut rng) + .map_err(IoFinalizerError::DummySignature)?; + } + } + + Ok(()) + } +} + +/// Errors that can occur while finalizing the I/O for a PCZT bundle. +#[derive(Debug)] +pub enum IoFinalizerError { + /// An error occurred while signing a dummy spend. + DummySignature(SignerError), + /// The IO Finalizer role requires all `rcv` fields to be set. + MissingValueCommitTrapdoor, + /// The `cv_net`, `rcv`, and `value_sum` values within the Orchard bundle are + /// inconsistent. + ValueCommitMismatch, +} diff --git a/src/pczt/parse.rs b/src/pczt/parse.rs new file mode 100644 index 000000000..62db8a1d3 --- /dev/null +++ b/src/pczt/parse.rs @@ -0,0 +1,329 @@ +use std::collections::BTreeMap; + +use ff::PrimeField; +use incrementalmerkletree::Hashable; +use pasta_curves::pallas; +use zcash_note_encryption::OutgoingCipherKey; +use zip32::ChildIndex; + +use super::{Action, Bundle, Output, Spend, Zip32Derivation}; +use crate::{ + bundle::Flags, + keys::{FullViewingKey, SpendingKey}, + note::{ExtractedNoteCommitment, Nullifier, RandomSeed, Rho, TransmittedNoteCiphertext}, + primitives::redpallas::{self, SpendAuth}, + tree::{MerkleHashOrchard, MerklePath}, + value::{NoteValue, Sign, ValueCommitTrapdoor, ValueCommitment, ValueSum}, + Address, Anchor, Proof, NOTE_COMMITMENT_TREE_DEPTH, +}; + +impl Bundle { + /// Parses a PCZT bundle from its component parts. + pub fn parse( + actions: Vec, + flags: u8, + value_sum: (u64, bool), + anchor: [u8; 32], + zkproof: Option>, + bsk: Option<[u8; 32]>, + ) -> Result { + let flags = Flags::from_byte(flags).ok_or(ParseError::UnexpectedFlagBitsSet)?; + + let value_sum = { + let (magnitude, is_negative) = value_sum; + ValueSum::from_magnitude_sign( + magnitude, + if is_negative { + Sign::Negative + } else { + Sign::Positive + }, + ) + }; + + let anchor = Anchor::from_bytes(anchor) + .into_option() + .ok_or(ParseError::InvalidAnchor)?; + + let zkproof = zkproof.map(Proof::new); + + let bsk = bsk + .map(redpallas::SigningKey::try_from) + .transpose() + .map_err(|_| ParseError::InvalidBindingSignatureSigningKey)?; + + Ok(Self { + actions, + flags, + value_sum, + anchor, + zkproof, + bsk, + }) + } +} + +impl Action { + /// Parses a PCZT action from its component parts. + pub fn parse( + cv_net: [u8; 32], + spend: Spend, + output: Output, + rcv: Option<[u8; 32]>, + ) -> Result { + let cv_net = ValueCommitment::from_bytes(&cv_net) + .into_option() + .ok_or(ParseError::InvalidValueCommitment)?; + + let rcv = rcv + .map(ValueCommitTrapdoor::from_bytes) + .map(|rcv| { + rcv.into_option() + .ok_or(ParseError::InvalidValueCommitTrapdoor) + }) + .transpose()?; + + Ok(Self { + cv_net, + spend, + output, + rcv, + }) + } +} + +impl Spend { + /// Parses a PCZT spend from its component parts. + pub fn parse( + nullifier: [u8; 32], + rk: [u8; 32], + spend_auth_sig: Option<[u8; 64]>, + recipient: Option<[u8; 43]>, + value: Option, + rho: Option<[u8; 32]>, + rseed: Option<[u8; 32]>, + fvk: Option<[u8; 96]>, + witness: Option<(u32, [[u8; 32]; NOTE_COMMITMENT_TREE_DEPTH])>, + alpha: Option<[u8; 32]>, + zip32_derivation: Option, + dummy_sk: Option<[u8; 32]>, + proprietary: BTreeMap>, + ) -> Result { + let nullifier = Nullifier::from_bytes(&nullifier) + .into_option() + .ok_or(ParseError::InvalidNullifier)?; + + let rk = redpallas::VerificationKey::try_from(rk) + .map_err(|_| ParseError::InvalidRandomizedKey)?; + + let spend_auth_sig = spend_auth_sig.map(redpallas::Signature::::from); + + let recipient = recipient + .as_ref() + .map(|r| { + Address::from_raw_address_bytes(r) + .into_option() + .ok_or(ParseError::InvalidRecipient) + }) + .transpose()?; + + let value = value.map(NoteValue::from_raw); + + let rho = rho + .map(|rho| { + Rho::from_bytes(&rho) + .into_option() + .ok_or(ParseError::InvalidRho) + }) + .transpose()?; + + let rseed = rseed + .map(|rseed| { + let rho = rho.as_ref().ok_or(ParseError::MissingRho)?; + RandomSeed::from_bytes(rseed, rho) + .into_option() + .ok_or(ParseError::InvalidRandomSeed) + }) + .transpose()?; + + let fvk = fvk + .map(|fvk| FullViewingKey::from_bytes(&fvk).ok_or(ParseError::InvalidFullViewingKey)) + .transpose()?; + + let witness = witness + .map(|(position, auth_path)| { + Ok(MerklePath::from_parts(position, { + // Replace this with `array::try_map` if it ever stabilises. + let mut buf = [MerkleHashOrchard::empty_leaf(); NOTE_COMMITMENT_TREE_DEPTH]; + for (from, to) in auth_path.into_iter().zip(&mut buf) { + *to = MerkleHashOrchard::from_bytes(&from) + .into_option() + .ok_or(ParseError::InvalidWitness)?; + } + buf + })) + }) + .transpose()?; + + let alpha = alpha + .map(|alpha| { + pallas::Scalar::from_repr(alpha) + .into_option() + .ok_or(ParseError::InvalidSpendAuthRandomizer) + }) + .transpose()?; + + let dummy_sk = dummy_sk + .map(|dummy_sk| { + SpendingKey::from_bytes(dummy_sk) + .into_option() + .ok_or(ParseError::InvalidDummySpendingKey) + }) + .transpose()?; + + Ok(Self { + nullifier, + rk, + spend_auth_sig, + recipient, + value, + rho, + rseed, + fvk, + witness, + alpha, + zip32_derivation, + dummy_sk, + proprietary, + }) + } +} + +impl Output { + /// Parses a PCZT output from its component parts, and the corresponding `Spend`'s + /// nullifier. + pub fn parse( + spend_nullifier: Nullifier, + cmx: [u8; 32], + ephemeral_key: [u8; 32], + enc_ciphertext: Vec, + out_ciphertext: Vec, + recipient: Option<[u8; 43]>, + value: Option, + rseed: Option<[u8; 32]>, + ock: Option<[u8; 32]>, + zip32_derivation: Option, + proprietary: BTreeMap>, + ) -> Result { + let cmx = ExtractedNoteCommitment::from_bytes(&cmx) + .into_option() + .ok_or(ParseError::InvalidExtractedNoteCommitment)?; + + let encrypted_note = TransmittedNoteCiphertext { + epk_bytes: ephemeral_key, + enc_ciphertext: enc_ciphertext + .as_slice() + .try_into() + .map_err(|_| ParseError::InvalidEncCiphertext)?, + out_ciphertext: out_ciphertext + .as_slice() + .try_into() + .map_err(|_| ParseError::InvalidOutCiphertext)?, + }; + + let recipient = recipient + .as_ref() + .map(|r| { + Address::from_raw_address_bytes(r) + .into_option() + .ok_or(ParseError::InvalidRecipient) + }) + .transpose()?; + + let value = value.map(NoteValue::from_raw); + + let rseed = rseed + .map(|rseed| { + let rho = Rho::from_nf_old(spend_nullifier); + RandomSeed::from_bytes(rseed, &rho) + .into_option() + .ok_or(ParseError::InvalidRandomSeed) + }) + .transpose()?; + + let ock = ock.map(OutgoingCipherKey); + + Ok(Self { + cmx, + encrypted_note, + recipient, + value, + rseed, + ock, + zip32_derivation, + proprietary, + }) + } +} + +impl Zip32Derivation { + /// Parses a ZIP 32 derivation path from its component parts. + /// + /// Returns an error if any of the derivation path indices are non-hardened (which + /// Orchard does not support). + pub fn parse( + seed_fingerprint: [u8; 32], + derivation_path: Vec, + ) -> Result { + Ok(Self { + seed_fingerprint, + derivation_path: derivation_path + .into_iter() + .map(|i| ChildIndex::from_index(i).ok_or(ParseError::InvalidZip32Derivation)) + .collect::>()?, + }) + } +} + +/// Errors that can occur while parsing a PCZT bundle. +#[derive(Debug)] +pub enum ParseError { + /// An invalid anchor was provided. + InvalidAnchor, + /// An invalid `bsk` was provided. + InvalidBindingSignatureSigningKey, + /// An invalid `dummy_sk` was provided. + InvalidDummySpendingKey, + /// An invalid `enc_ciphertext` was provided. + InvalidEncCiphertext, + /// An invalid `cmx` was provided. + InvalidExtractedNoteCommitment, + /// An invalid `fvk` was provided. + InvalidFullViewingKey, + /// An invalid `nullifier` was provided. + InvalidNullifier, + /// An invalid `out_ciphertext` was provided. + InvalidOutCiphertext, + /// An invalid `rk` was provided. + InvalidRandomizedKey, + /// An invalid `rseed` was provided. + InvalidRandomSeed, + /// An invalid `recipient` was provided. + InvalidRecipient, + /// An invalid `rho` was provided. + InvalidRho, + /// An invalid `alpha` was provided. + InvalidSpendAuthRandomizer, + /// An invalid `cv_net` was provided. + InvalidValueCommitment, + /// An invalid `rcv` was provided. + InvalidValueCommitTrapdoor, + /// An invalid `witness` was provided. + InvalidWitness, + /// An invalid `zip32_derivation` was provided. + InvalidZip32Derivation, + /// `rho` must be provided whenever `rseed` is provided. + MissingRho, + /// The provided `flags` field had unexpected bits set. + UnexpectedFlagBitsSet, +} diff --git a/src/pczt/prover.rs b/src/pczt/prover.rs new file mode 100644 index 000000000..03da7e364 --- /dev/null +++ b/src/pczt/prover.rs @@ -0,0 +1,129 @@ +use halo2_proofs::plonk; +use rand::{CryptoRng, RngCore}; + +use crate::{ + builder::SpendInfo, + circuit::{Circuit, Instance, ProvingKey}, + note::Rho, + Note, Proof, +}; + +impl super::Bundle { + /// Adds a proof to this PCZT bundle. + pub fn create_proof( + &mut self, + pk: &ProvingKey, + rng: R, + ) -> Result<(), ProverError> { + let circuits = self + .actions + .iter() + .map(|action| { + let fvk = action + .spend + .fvk + .clone() + .ok_or(ProverError::MissingFullViewingKey)?; + + let note = Note::from_parts( + action + .spend + .recipient + .ok_or(ProverError::MissingRecipient)?, + action.spend.value.ok_or(ProverError::MissingValue)?, + action.spend.rho.ok_or(ProverError::MissingRho)?, + action.spend.rseed.ok_or(ProverError::MissingRandomSeed)?, + ) + .into_option() + .ok_or(ProverError::InvalidSpendNote)?; + + let merkle_path = action + .spend + .witness + .clone() + .ok_or(ProverError::MissingWitness)?; + + let spend = + SpendInfo::new(fvk, note, merkle_path).ok_or(ProverError::WrongFvkForNote)?; + + let output_note = Note::from_parts( + action + .output + .recipient + .ok_or(ProverError::MissingRecipient)?, + action.output.value.ok_or(ProverError::MissingValue)?, + Rho::from_nf_old(action.spend.nullifier), + action.output.rseed.ok_or(ProverError::MissingRandomSeed)?, + ) + .into_option() + .ok_or(ProverError::InvalidOutputNote)?; + + let alpha = action + .spend + .alpha + .ok_or(ProverError::MissingSpendAuthRandomizer)?; + let rcv = action + .rcv + .clone() + .ok_or(ProverError::MissingValueCommitTrapdoor)?; + + Circuit::from_action_context(spend, output_note, alpha, rcv) + .ok_or(ProverError::RhoMismatch) + }) + .collect::, ProverError>>()?; + + let instances = self + .actions + .iter() + .map(|action| { + Instance::from_parts( + self.anchor, + action.cv_net.clone(), + action.spend.nullifier, + action.spend.rk.clone(), + action.output.cmx, + self.flags.spends_enabled(), + self.flags.outputs_enabled(), + ) + }) + .collect::>(); + + let proof = + Proof::create(pk, &circuits, &instances, rng).map_err(ProverError::ProofFailed)?; + + self.zkproof = Some(proof); + + Ok(()) + } +} + +/// Errors that can occur while creating Orchard proofs for a PCZT. +#[derive(Debug)] +pub enum ProverError { + /// The output note's components do not produce a valid note commitment. + InvalidOutputNote, + /// The spent note's components do not produce a valid note commitment. + InvalidSpendNote, + /// The Prover role requires `fvk` to be set. + MissingFullViewingKey, + /// The Prover role requires all `rseed` fields to be set. + MissingRandomSeed, + /// The Prover role requires all `recipient` fields to be set. + MissingRecipient, + /// The Prover role requires `rho` to be set. + MissingRho, + /// The Prover role requires `alpha` to be set. + MissingSpendAuthRandomizer, + /// The Prover role requires all `value` fields to be set. + MissingValue, + /// The Prover role requires `rcv` to be set. + MissingValueCommitTrapdoor, + /// The Prover role requires `witness` to be set. + MissingWitness, + /// An error occurred while creating the proof. + ProofFailed(plonk::Error), + /// The `rho` of the `output_note` is not equal to the nullifier of the spent note. + RhoMismatch, + /// The provided `fvk` does not own the spent note. + WrongFvkForNote, +} diff --git a/src/pczt/signer.rs b/src/pczt/signer.rs new file mode 100644 index 000000000..7f5f8028c --- /dev/null +++ b/src/pczt/signer.rs @@ -0,0 +1,41 @@ +use rand::{CryptoRng, RngCore}; + +use crate::{keys::SpendAuthorizingKey, primitives::redpallas}; + +impl super::Action { + /// Signs the Orchard spend 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( + &mut self, + sighash: [u8; 32], + ask: &SpendAuthorizingKey, + rng: R, + ) -> Result<(), SignerError> { + let alpha = self + .spend + .alpha + .ok_or(SignerError::MissingSpendAuthRandomizer)?; + + let rsk = ask.randomize(&alpha); + let rk = redpallas::VerificationKey::from(&rsk); + + if self.spend.rk == rk { + self.spend.spend_auth_sig = Some(rsk.sign(rng, &sighash)); + Ok(()) + } else { + Err(SignerError::WrongSpendAuthorizingKey) + } + } +} + +/// Errors that can occur while signing an Orchard action in a PCZT. +#[derive(Debug)] +pub enum SignerError { + /// The Signer role requires `alpha` to be set. + MissingSpendAuthRandomizer, + /// The provided `ask` does not own the action's spent note. + WrongSpendAuthorizingKey, +} diff --git a/src/pczt/tx_extractor.rs b/src/pczt/tx_extractor.rs new file mode 100644 index 000000000..ce2d9ccbe --- /dev/null +++ b/src/pczt/tx_extractor.rs @@ -0,0 +1,152 @@ +use nonempty::NonEmpty; +use rand::{CryptoRng, RngCore}; + +use super::Action; +use crate::{ + bundle::{Authorization, Authorized, EffectsOnly}, + primitives::redpallas::{self, Binding, SpendAuth}, + Proof, +}; + +impl super::Bundle { + /// Extracts the effects of this PCZT bundle as a [regular `Bundle`]. + /// + /// This is used by the Signer role to produce the transaction sighash. + /// + /// [regular `Bundle`]: crate::Bundle + pub fn extract_effects>( + &self, + ) -> Result>, TxExtractorError> { + self.to_tx_data(|_| Ok(()), |_| Ok(EffectsOnly)) + } + + /// Extracts a fully authorized [regular `Bundle`] from this PCZT bundle. + /// + /// This is used by the Transaction Extractor role to produce the final transaction. + /// + /// [regular `Bundle`]: crate::Bundle + pub fn extract>( + self, + ) -> Result>, TxExtractorError> { + self.to_tx_data( + |action| { + Ok(action + .spend + .spend_auth_sig + .clone() + .ok_or(TxExtractorError::MissingSpendAuthSig)?) + }, + |bundle| { + Ok(Unbound { + proof: bundle + .zkproof + .clone() + .ok_or(TxExtractorError::MissingProof)?, + bsk: bundle + .bsk + .clone() + .ok_or(TxExtractorError::MissingBindingSignatureSigningKey)?, + }) + }, + ) + } + + /// Converts this PCZT bundle into a regular bundle with the given authorizations. + fn to_tx_data( + &self, + action_auth: F, + bundle_auth: G, + ) -> Result>, E> + where + A: Authorization, + E: From, + F: Fn(&Action) -> Result<::SpendAuth, E>, + G: FnOnce(&Self) -> Result, + V: TryFrom, + { + let actions = self + .actions + .iter() + .map(|action| { + let authorization = action_auth(action)?; + + Ok(crate::Action::from_parts( + action.spend.nullifier, + action.spend.rk.clone(), + action.output.cmx, + action.output.encrypted_note.clone(), + action.cv_net.clone(), + authorization, + )) + }) + .collect::>()?; + + Ok(if let Some(actions) = NonEmpty::from_vec(actions) { + let value_balance = i64::try_from(self.value_sum) + .ok() + .and_then(|v| v.try_into().ok()) + .ok_or(TxExtractorError::ValueSumOutOfRange)?; + + let authorization = bundle_auth(&self)?; + + Some(crate::Bundle::from_parts( + actions, + self.flags, + value_balance, + self.anchor, + authorization, + )) + } else { + None + }) + } +} + +/// Errors that can occur while extracting a regular Orchard bundle from a PCZT bundle. +#[derive(Debug)] +pub enum TxExtractorError { + /// The Transaction Extractor role requires `bsk` to be set. + MissingBindingSignatureSigningKey, + /// The Transaction Extractor role requires `zkproof` to be set. + MissingProof, + /// The Transaction Extractor role requires all `spend_auth_sig` fields to be set. + MissingSpendAuthSig, + /// The value sum does not fit into a `valueBalance`. + ValueSumOutOfRange, +} + +/// Authorizing data for a bundle of actions that is just missing a binding signature. +#[derive(Debug)] +pub struct Unbound { + proof: Proof, + bsk: redpallas::SigningKey, +} + +impl Authorization for Unbound { + type SpendAuth = redpallas::Signature; +} + +impl crate::Bundle { + /// Verifies the given sighash with every `spend_auth_sig`, and then binds the bundle. + /// + /// Returns `None` if the given sighash does not validate against every `spend_auth_sig`. + pub fn apply_binding_signature( + self, + sighash: [u8; 32], + rng: R, + ) -> Option> { + if self + .actions() + .iter() + .all(|action| action.rk().verify(&sighash, action.authorization()).is_ok()) + { + Some(self.map_authorization( + &mut (), + |_, _, a| a, + |_, Unbound { proof, bsk }| Authorized::from_parts(proof, bsk.sign(rng, &sighash)), + )) + } else { + None + } + } +} diff --git a/src/tree.rs b/src/tree.rs index 352974672..046f5d924 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -84,7 +84,7 @@ impl Anchor { /// The Merkle path from a leaf of the note commitment tree /// to its anchor. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct MerklePath { position: u32, auth_path: [MerkleHashOrchard; MERKLE_DEPTH_ORCHARD], @@ -159,12 +159,12 @@ impl MerklePath { } /// Returns the position of the leaf using this Merkle path. - pub(crate) fn position(&self) -> u32 { + pub fn position(&self) -> u32 { self.position } /// Returns the authentication path. - pub(crate) fn auth_path(&self) -> [MerkleHashOrchard; MERKLE_DEPTH_ORCHARD] { + pub fn auth_path(&self) -> [MerkleHashOrchard; MERKLE_DEPTH_ORCHARD] { self.auth_path } } diff --git a/src/value.rs b/src/value.rs index 9ac440eeb..61c22337d 100644 --- a/src/value.rs +++ b/src/value.rs @@ -138,8 +138,12 @@ impl Sub for NoteValue { } } -pub(crate) enum Sign { +/// The sign of a [`ValueSum`]. +#[derive(Debug)] +pub enum Sign { + /// A non-negative [`ValueSum`]. Positive, + /// A negative [`ValueSum`]. Negative, } @@ -163,8 +167,23 @@ impl ValueSum { ValueSum(value as i128) } + /// Constructs a value sum from its magnitude and sign. + pub(crate) fn from_magnitude_sign(magnitude: u64, sign: Sign) -> Self { + Self(match sign { + Sign::Positive => magnitude as i128, + Sign::Negative => -(magnitude as i128), + }) + } + /// Splits this value sum into its magnitude and sign. - pub(crate) fn magnitude_sign(&self) -> (u64, Sign) { + /// + /// This is a low-level API, requiring a detailed understanding of the + /// [use of value balancing][orchardbalance] in the Zcash protocol to use correctly + /// and securely. It is intended to be used in combination with the [`crate::pczt`] + /// module. + /// + /// [orchardbalance]: https://zips.z.cash/protocol/protocol.pdf#orchardbalance + pub fn magnitude_sign(&self) -> (u64, Sign) { let (magnitude, sign) = if self.0.is_negative() { (-self.0, Sign::Negative) } else { @@ -232,6 +251,18 @@ impl ValueCommitTrapdoor { pub fn from_bytes(bytes: [u8; 32]) -> CtOption { pallas::Scalar::from_repr(bytes).map(ValueCommitTrapdoor) } + + /// Returns the byte encoding of a `ValueCommitTrapdoor`. + /// + /// This is a low-level API, requiring a detailed understanding of the + /// [use of value commitment trapdoors][orchardbalance] in the Zcash protocol + /// to use correctly and securely. It is intended to be used in combination + /// with the [`crate::pczt`] module. + /// + /// [orchardbalance]: https://zips.z.cash/protocol/protocol.pdf#orchardbalance + pub fn to_bytes(&self) -> [u8; 32] { + self.0.to_repr() + } } impl Add<&ValueCommitTrapdoor> for ValueCommitTrapdoor {