diff --git a/src/builder.rs b/src/builder.rs index 850534350..5e37c1593 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -20,7 +20,6 @@ use crate::{ SpendingKey, }, note::{AssetBase, ExtractedNoteCommitment, Note, Nullifier, Rho, TransmittedNoteCiphertext}, - orchard_flavor::OrchardVanilla, orchard_sighash_versioning::{VerBindingSig, VerSpendAuthSig}, primitives::redpallas::{self, Binding, SpendAuth}, primitives::{OrchardDomain, OrchardPrimitives}, @@ -351,9 +350,6 @@ impl SpendInfo { } fn into_pczt(self, rng: impl RngCore) -> crate::pczt::Spend { - assert!(!self.split_flag); - assert_eq!(self.note.asset(), AssetBase::native()); - let (nf_old, _, alpha, rk) = self.build(rng); crate::pczt::Spend { @@ -362,11 +358,14 @@ impl SpendInfo { spend_auth_sig: None, recipient: Some(self.note.recipient()), value: Some(self.note.value()), + asset: Some(self.note.asset()), rho: Some(self.note.rho()), rseed: Some(*self.note.rseed()), + rseed_split_note: self.note.rseed_split_note().into(), fvk: Some(self.fvk), witness: Some(self.merkle_path), alpha: Some(alpha), + split_flag: Some(self.split_flag), zip32_derivation: None, dummy_sk: self.dummy_sk, proprietary: BTreeMap::new(), @@ -439,19 +438,17 @@ impl OutputInfo { (note, cmx, encrypted_note) } - fn into_pczt( + fn into_pczt( self, cv_net: &ValueCommitment, nf_old: Nullifier, rng: impl RngCore, ) -> crate::pczt::Output { - assert_eq!(self.asset, AssetBase::native()); - - let (note, cmx, encrypted_note) = self.build::(cv_net, nf_old, rng); + let (note, cmx, encrypted_note) = self.build::

(cv_net, nf_old, rng); crate::pczt::Output { cmx, - encrypted_note, + encrypted_note: encrypted_note.into(), recipient: Some(self.recipient), value: Some(self.value), rseed: Some(*note.rseed()), @@ -536,7 +533,7 @@ impl ActionInfo { ) } - fn build_for_pczt(self, mut rng: impl RngCore) -> crate::pczt::Action { + fn build_for_pczt(self, mut rng: impl RngCore) -> crate::pczt::Action { assert_eq!( self.spend.note.asset(), self.output.asset, @@ -546,7 +543,9 @@ impl ActionInfo { let cv_net = ValueCommitment::derive(v_net, self.rcv, self.spend.note.asset()); let spend = self.spend.into_pczt(&mut rng); - let output = self.output.into_pczt(&cv_net, spend.nullifier, &mut rng); + let output = self + .output + .into_pczt::

(&cv_net, spend.nullifier, &mut rng); crate::pczt::Action { cv_net, @@ -777,7 +776,7 @@ impl Builder { /// Builds a bundle containing the given spent notes and outputs along with their /// metadata, for inclusion in a PCZT. - pub fn build_for_pczt( + pub fn build_for_pczt( self, rng: impl RngCore, ) -> Result<(crate::pczt::Bundle, BundleMetadata), BuildError> { @@ -788,11 +787,11 @@ impl Builder { self.spends, self.outputs, self.burn, - |pre_actions, flags, value_sum, _burn_vec, bundle_meta, mut rng| { + |pre_actions, flags, value_sum, burn_vec, bundle_meta, mut rng| { // Create the actions. let actions = pre_actions .into_iter() - .map(|a| a.build_for_pczt(&mut rng)) + .map(|a| a.build_for_pczt::

(&mut rng)) .collect::>(); Ok(( @@ -801,6 +800,8 @@ impl Builder { flags, value_sum, anchor: self.anchor, + burn: burn_vec, + expiry_height: 0, zkproof: None, bsk: None, }, diff --git a/src/pczt.rs b/src/pczt.rs index 8a924cf70..a542a547c 100644 --- a/src/pczt.rs +++ b/src/pczt.rs @@ -7,15 +7,20 @@ use core::fmt; use getset::Getters; use pasta_curves::pallas; -use zcash_note_encryption::OutgoingCipherKey; +use zcash_note_encryption::{note_bytes::NoteBytes, OutgoingCipherKey}; use zip32::ChildIndex; use crate::{ bundle::Flags, keys::{FullViewingKey, SpendingKey}, - note::{ExtractedNoteCommitment, Nullifier, RandomSeed, Rho, TransmittedNoteCiphertext}, - orchard_flavor::OrchardVanilla, - primitives::redpallas::{self, Binding, SpendAuth}, + note::{ + AssetBase, ExtractedNoteCommitment, Nullifier, RandomSeed, Rho, TransmittedNoteCiphertext, + }, + orchard_sighash_versioning::VerSpendAuthSig, + primitives::{ + redpallas::{self, Binding, SpendAuth}, + OrchardPrimitives, + }, tree::MerklePath, value::{NoteValue, ValueCommitTrapdoor, ValueCommitment, ValueSum}, Address, Anchor, Proof, @@ -72,11 +77,24 @@ pub struct Bundle { /// redacted from the PCZT after they are no longer necessary. pub(crate) value_sum: ValueSum, + /// Assets intended for burning + /// + /// Set by the Constructor. + pub(crate) burn: Vec<(AssetBase, NoteValue)>, + /// The Orchard anchor for this transaction. /// /// Set by the Creator. pub(crate) anchor: Anchor, + /// Block height after which this Bundle's Actions are invalid by consensus. + /// + /// For the OrchardZSA protocol, `expiry_height` is set to 0, indicating no expiry. + /// This field is reserved for future use. + /// + /// Set by the Constructor. + pub(crate) expiry_height: u32, + /// The Orchard bundle proof. /// /// This is `None` until it is set by the Prover. @@ -141,10 +159,10 @@ pub struct Spend { /// The randomized verification key for the note being spent. pub(crate) rk: redpallas::VerificationKey, - /// The spend authorization signature. + /// The versioned spend authorization signature. /// /// This is set by the Signer. - pub(crate) spend_auth_sig: Option>, + pub(crate) spend_auth_sig: Option, /// The address that received the note being spent. /// @@ -162,6 +180,12 @@ pub struct Spend { /// information, or after signatures have been applied, this can be redacted. pub(crate) value: Option, + /// The asset of this Action. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + pub(crate) asset: Option, + /// The rho value for the note being spent. /// /// - This is set by the Constructor. @@ -177,6 +201,12 @@ pub struct Spend { /// - This is required by the Prover. pub(crate) rseed: Option, + /// The seed randomness for split notes. + /// + /// - This is set by the Constructor. + /// - This is required by the Prover. + pub(crate) rseed_split_note: Option, + /// The full viewing key that received the note being spent. /// /// - This is set by the Updater. @@ -197,6 +227,12 @@ pub struct Spend { /// - After`zkproof` / `spend_auth_sig` has been set, this can be redacted. pub(crate) alpha: Option, + /// A flag to indicate whether the value of the SpendInfo will be counted in the `ValueSum` of the action. + /// + /// - This is chosen by the Constructor. + /// - This is required by the Prover. + pub(crate) split_flag: Option, + /// The ZIP 32 derivation path at which the spending key can be found for the note /// being spent. pub(crate) zip32_derivation: Option, @@ -225,7 +261,7 @@ pub struct Output { /// - `ephemeral_key` /// - `enc_ciphertext` /// - `out_ciphertext` - pub(crate) encrypted_note: TransmittedNoteCiphertext, + pub(crate) encrypted_note: PcztTransmittedNoteCiphertext, /// The address that will receive the output. /// @@ -331,41 +367,75 @@ impl Zip32Derivation { } } +/// An encrypted note. +#[derive(Clone, Debug)] +pub struct PcztTransmittedNoteCiphertext { + /// The serialization of the ephemeral public key + pub epk_bytes: [u8; 32], + /// The encrypted note ciphertext + pub enc_ciphertext: Vec, + /// An encrypted value that allows the holder of the outgoing cipher + /// key for the note to recover the note plaintext. + pub out_ciphertext: [u8; 80], +} + +impl From> for PcztTransmittedNoteCiphertext { + fn from(transmitted: TransmittedNoteCiphertext

) -> Self { + PcztTransmittedNoteCiphertext { + epk_bytes: transmitted.epk_bytes, + enc_ciphertext: transmitted.enc_ciphertext.as_ref().to_vec(), + out_ciphertext: transmitted.out_ciphertext, + } + } +} + +impl From for TransmittedNoteCiphertext

{ + fn from(ciphertext: PcztTransmittedNoteCiphertext) -> Self { + TransmittedNoteCiphertext { + epk_bytes: ciphertext.epk_bytes, + enc_ciphertext: P::NoteCiphertextBytes::from_slice(&ciphertext.enc_ciphertext) + .expect("Failed to parse enc_ciphertext: data may be corrupt or incorrect size"), + out_ciphertext: ciphertext.out_ciphertext, + } + } +} + #[cfg(test)] mod tests { + use blake2b_simd::Hash as Blake2bHash; use ff::{Field, PrimeField}; use incrementalmerkletree::{Marking, Retention}; + use nonempty::NonEmpty; use pasta_curves::pallas; - use rand::rngs::OsRng; + use rand::{rngs::StdRng, SeedableRng}; use shardtree::{store::memory::MemoryShardStore, ShardTree}; use crate::{ builder::{Builder, BundleType}, + bundle::commitments::hash_bundle_txid_data, circuit::ProvingKey, constants::MERKLE_DEPTH_ORCHARD, + issuance::compute_asset_desc_hash, + issuance_auth::{IssueAuthKey, IssueValidatingKey, ZSASchnorr}, keys::{FullViewingKey, Scope, SpendAuthorizingKey, SpendingKey}, note::{AssetBase, ExtractedNoteCommitment, RandomSeed, Rho}, - orchard_flavor::OrchardVanilla, + orchard_flavor::{OrchardFlavor, OrchardVanilla, OrchardZSA}, pczt::Zip32Derivation, tree::{MerkleHashOrchard, EMPTY_ROOTS}, value::NoteValue, Note, }; - #[test] - fn shielding_bundle() { - let pk = ProvingKey::build::(); - let mut rng = OsRng; + fn shielding_bundle(bundle_type: BundleType) -> Blake2bHash { + let pk = ProvingKey::build::(); + let mut rng = StdRng::seed_from_u64(1u64); 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_VANILLA, - EMPTY_ROOTS[MERKLE_DEPTH_ORCHARD].into(), - ); + let mut builder = Builder::new(bundle_type, EMPTY_ROOTS[MERKLE_DEPTH_ORCHARD].into()); builder .add_output( None, @@ -377,27 +447,77 @@ mod tests { .unwrap(); let balance: i64 = builder.value_balance().unwrap(); assert_eq!(balance, -5000); - let mut pczt_bundle = builder.build_for_pczt(&mut rng).unwrap().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, rng).unwrap(); + pczt_bundle.finalize_io(sighash, rng.clone()).unwrap(); // Run the Prover role. - pczt_bundle.create_proof(&pk, rng).unwrap(); + pczt_bundle.create_proof::(&pk, rng.clone()).unwrap(); // Run the Transaction Extractor role. - let bundle = pczt_bundle.extract::().unwrap().unwrap(); + let bundle = pczt_bundle.extract::().unwrap().unwrap(); + + let orchard_digest = hash_bundle_txid_data(&bundle); assert_eq!(bundle.value_balance(), &(-5000)); // We can successfully bind the bundle. bundle.apply_binding_signature(sighash, rng).unwrap(); + + orchard_digest + } + + #[test] + fn shielding_bundle_orchard_zsa() { + let orchard_digest = shielding_bundle::(BundleType::DEFAULT_ZSA); + assert_eq!( + orchard_digest.as_bytes(), + // Locks the `orchard_digest` for OrchardZSA + &[ + 164, 99, 147, 6, 19, 86, 66, 138, 59, 232, 52, 111, 235, 197, 66, 151, 86, 53, 65, + 250, 3, 129, 217, 11, 97, 160, 68, 75, 195, 188, 76, 161 + ], + ); + let orchard_digest = shielding_bundle::(BundleType::DEFAULT_VANILLA); + assert_eq!( + orchard_digest.as_bytes(), + // Locks the `orchard_digest` for OrchardZSA + &[ + 208, 245, 141, 8, 182, 182, 91, 48, 64, 24, 222, 106, 63, 72, 11, 99, 216, 111, + 114, 94, 36, 153, 68, 253, 137, 240, 20, 184, 160, 192, 10, 155 + ], + ); } #[test] - fn shielded_bundle() { - let pk = ProvingKey::build::(); - let mut rng = OsRng; + fn shielding_bundle_orchard_vanilla() { + let orchard_digest = shielding_bundle::(BundleType::DEFAULT_VANILLA); + assert_eq!( + orchard_digest.as_bytes(), + // `orchard_digest` taken from the `zcash/orchard` repository at commit `4ac248d0` (v0.11.0) + // This ensures backward compatibility. + &[ + 35, 70, 47, 40, 125, 172, 163, 93, 38, 0, 111, 90, 137, 81, 41, 181, 216, 25, 190, + 222, 210, 45, 44, 127, 182, 44, 228, 229, 132, 64, 53, 7, + ], + ); + } + + fn shielded_bundle(bundle_type: BundleType, zsa_asset: bool) -> Blake2bHash { + let pk = ProvingKey::build::(); + let mut rng = StdRng::seed_from_u64(1u64); + + let asset = if zsa_asset { + // Create a ZSA asset from its issue validating key. + let isk = IssueAuthKey::::random(&mut rng); + let ik = IssueValidatingKey::from(&isk); + let asset_desc_hash = + compute_asset_desc_hash(&NonEmpty::from_slice(b"asset1").unwrap()); + AssetBase::derive(&ik, &asset_desc_hash) + } else { + AssetBase::native() + }; // Pretend we derived the spending key via ZIP 32. let zip32_derivation = Zip32Derivation::parse([1; 32], vec![]).unwrap(); @@ -414,7 +534,7 @@ mod tests { if let Some(note) = Note::from_parts( recipient, value, - AssetBase::native(), + asset, rho, RandomSeed::random(&mut rng, &rho), ) @@ -450,7 +570,7 @@ mod tests { }; // Run the Creator and Constructor roles. - let mut builder = Builder::new(BundleType::DEFAULT_VANILLA, anchor); + let mut builder = Builder::new(bundle_type, anchor); builder .add_spend(fvk.clone(), note, merkle_path.into()) .unwrap(); @@ -459,7 +579,7 @@ mod tests { None, recipient, NoteValue::from_raw(10_000), - AssetBase::native(), + asset, [0u8; 512], ) .unwrap(); @@ -468,17 +588,17 @@ mod tests { Some(fvk.to_ovk(Scope::Internal)), fvk.address_at(0u32, Scope::Internal), NoteValue::from_raw(5_000), - AssetBase::native(), + asset, [0u8; 512], ) .unwrap(); let balance: i64 = builder.value_balance().unwrap(); assert_eq!(balance, 0); - let mut pczt_bundle = builder.build_for_pczt(&mut rng).unwrap().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, rng).unwrap(); + pczt_bundle.finalize_io(sighash, rng.clone()).unwrap(); // Run the Updater role. for action in pczt_bundle.actions_mut() { @@ -491,7 +611,7 @@ mod tests { } // Run the Prover role. - pczt_bundle.create_proof(&pk, rng).unwrap(); + pczt_bundle.create_proof::(&pk, rng.clone()).unwrap(); // TODO: Verify that the PCZT contains sufficient information to decrypt and check // `enc_ciphertext`. @@ -499,15 +619,73 @@ mod tests { // 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, rng).unwrap(); + action.sign(sighash, &ask, rng.clone()).unwrap(); } } + // Verify the PCZT bundle before extraction. + pczt_bundle + .actions + .iter() + .for_each(|action| action.verify_cv_net().unwrap()); + pczt_bundle + .actions + .iter() + .for_each(|action| action.spend.verify_nullifier(Some(&fvk)).unwrap()); + pczt_bundle + .actions + .iter() + .for_each(|action| action.spend.verify_rk(Some(&fvk)).unwrap()); + pczt_bundle + .actions + .iter() + .for_each(|action| action.output.verify_note_commitment(&action.spend).unwrap()); + // Run the Transaction Extractor role. - let bundle = pczt_bundle.extract::().unwrap().unwrap(); + let bundle = pczt_bundle.extract::().unwrap().unwrap(); + + let orchard_digest = hash_bundle_txid_data(&bundle); assert_eq!(bundle.value_balance(), &0); // We can successfully bind the bundle. bundle.apply_binding_signature(sighash, rng).unwrap(); + + orchard_digest + } + + #[test] + fn shielded_bundle_orchard_zsa() { + let orchard_digest = shielded_bundle::(BundleType::DEFAULT_ZSA, true); + assert_eq!( + orchard_digest.as_bytes(), + // Locks the `orchard_digest` for OrchardZSA + &[ + 144, 163, 81, 183, 182, 35, 14, 118, 136, 151, 227, 118, 194, 19, 150, 182, 12, 69, + 84, 141, 149, 253, 27, 56, 110, 185, 93, 33, 250, 222, 97, 91 + ], + ); + let orchard_digest = shielded_bundle::(BundleType::DEFAULT_VANILLA, false); + assert_eq!( + orchard_digest.as_bytes(), + // Locks the `orchard_digest` for OrchardZSA + &[ + 34, 179, 215, 77, 244, 97, 43, 180, 112, 169, 87, 174, 23, 157, 98, 43, 72, 248, + 198, 6, 24, 193, 245, 116, 123, 100, 230, 178, 196, 32, 5, 103 + ], + ); + } + + #[test] + fn shielded_bundle_orchard_vanilla() { + let orchard_digest = shielded_bundle::(BundleType::DEFAULT_VANILLA, false); + assert_eq!( + orchard_digest.as_bytes(), + // `orchard_digest` taken from the `zcash/orchard` repository at commit `4ac248d0` (v0.11.0) + // This ensures backward compatibility. + &[ + 231, 253, 96, 110, 94, 252, 150, 231, 45, 160, 60, 178, 10, 219, 95, 29, 113, 154, + 223, 96, 50, 247, 213, 119, 211, 232, 186, 59, 24, 93, 177, 225 + ], + ); } } diff --git a/src/pczt/io_finalizer.rs b/src/pczt/io_finalizer.rs index 7474d428e..8602e04ca 100644 --- a/src/pczt/io_finalizer.rs +++ b/src/pczt/io_finalizer.rs @@ -36,7 +36,7 @@ impl super::Bundle { .map(|a| a.cv_net().clone()) .collect::>(), self.value_sum, - &[], + &self.burn, ); if redpallas::VerificationKey::from(&bsk) != bvk { return Err(IoFinalizerError::ValueCommitMismatch); diff --git a/src/pczt/parse.rs b/src/pczt/parse.rs index 76414d908..5be6654a8 100644 --- a/src/pczt/parse.rs +++ b/src/pczt/parse.rs @@ -5,19 +5,16 @@ use alloc::vec::Vec; use ff::PrimeField; use incrementalmerkletree::Hashable; use pasta_curves::pallas; -use zcash_note_encryption::{note_bytes::NoteBytes, OutgoingCipherKey}; +use zcash_note_encryption::OutgoingCipherKey; use zip32::ChildIndex; -use super::{Action, Bundle, Output, Spend, Zip32Derivation}; +use super::{Action, Bundle, Output, PcztTransmittedNoteCiphertext, Spend, Zip32Derivation}; use crate::{ bundle::Flags, keys::{FullViewingKey, SpendingKey}, - note::{ExtractedNoteCommitment, Nullifier, RandomSeed, Rho, TransmittedNoteCiphertext}, - orchard_flavor::OrchardVanilla, - primitives::{ - redpallas::{self, SpendAuth}, - OrchardPrimitives, - }, + note::{AssetBase, ExtractedNoteCommitment, Nullifier, RandomSeed, Rho}, + orchard_sighash_versioning::{OrchardSighashVersion, VerSpendAuthSig}, + primitives::redpallas::{self, SpendAuth}, tree::{MerkleHashOrchard, MerklePath}, value::{NoteValue, Sign, ValueCommitTrapdoor, ValueCommitment, ValueSum}, Address, Anchor, Proof, NOTE_COMMITMENT_TREE_DEPTH, @@ -26,11 +23,14 @@ use crate::{ impl Bundle { /// Parses a PCZT bundle from its component parts. /// `value_sum` is represented as `(magnitude, is_negative)`. + #[allow(clippy::too_many_arguments)] pub fn parse( actions: Vec, flags: u8, value_sum: (u64, bool), anchor: [u8; 32], + burn: Vec<([u8; 32], u64)>, + expiry_height: u32, zkproof: Option>, bsk: Option<[u8; 32]>, ) -> Result { @@ -52,6 +52,17 @@ impl Bundle { .into_option() .ok_or(ParseError::InvalidAnchor)?; + let burn = burn + .into_iter() + .map(|(burn_asset, burn_value)| { + let burn_asset = AssetBase::from_bytes(&burn_asset) + .into_option() + .ok_or(ParseError::InvalidBurn)?; + let burn_value = NoteValue::from_raw(burn_value); + Ok((burn_asset, burn_value)) + }) + .collect::, _>>()?; + let zkproof = zkproof.map(Proof::new); let bsk = bsk @@ -64,6 +75,8 @@ impl Bundle { flags, value_sum, anchor, + burn, + expiry_height, zkproof, bsk, }) @@ -99,20 +112,32 @@ impl Action { } } +/// Converts an unsigned 8-bit integer into an `Option`. +fn orchard_sighash_version_from_u8(n: u8) -> Option { + match n { + 0 => Some(OrchardSighashVersion::V0), + u8::MAX => Some(OrchardSighashVersion::NoVersion), + _ => None, + } +} + impl Spend { /// Parses a PCZT spend from its component parts. #[allow(clippy::too_many_arguments)] pub fn parse( nullifier: [u8; 32], rk: [u8; 32], - spend_auth_sig: Option<[u8; 64]>, + spend_auth_sig: Option<(u8, [u8; 64])>, recipient: Option<[u8; 43]>, value: Option, + asset: Option<[u8; 32]>, rho: Option<[u8; 32]>, rseed: Option<[u8; 32]>, + rseed_split_note: Option<[u8; 32]>, fvk: Option<[u8; 96]>, witness: Option<(u32, [[u8; 32]; NOTE_COMMITMENT_TREE_DEPTH])>, alpha: Option<[u8; 32]>, + split_flag: Option, zip32_derivation: Option, dummy_sk: Option<[u8; 32]>, proprietary: BTreeMap>, @@ -124,7 +149,15 @@ impl Spend { let rk = redpallas::VerificationKey::try_from(rk) .map_err(|_| ParseError::InvalidRandomizedKey)?; - let spend_auth_sig = spend_auth_sig.map(redpallas::Signature::::from); + let spend_auth_sig = spend_auth_sig + .as_ref() + .map(|(version, sig)| { + let version = orchard_sighash_version_from_u8(*version) + .ok_or(ParseError::InvalidSighashVersion)?; + let sig = redpallas::Signature::::from(*sig); + Ok(VerSpendAuthSig::new(version, sig)) + }) + .transpose()?; let recipient = recipient .as_ref() @@ -137,6 +170,9 @@ impl Spend { let value = value.map(NoteValue::from_raw); + let asset: Option = + asset.and_then(|bytes| AssetBase::from_bytes(&bytes).into_option()); + let rho = rho .map(|rho| { Rho::from_bytes(&rho) @@ -154,6 +190,15 @@ impl Spend { }) .transpose()?; + let rseed_split_note = rseed_split_note + .map(|rseed_split_note| { + let rho = rho.as_ref().ok_or(ParseError::MissingRho)?; + RandomSeed::from_bytes(rseed_split_note, rho) + .into_option() + .ok_or(ParseError::InvalidRandomSeed) + }) + .transpose()?; + let fvk = fvk .map(|fvk| FullViewingKey::from_bytes(&fvk).ok_or(ParseError::InvalidFullViewingKey)) .transpose()?; @@ -195,11 +240,14 @@ impl Spend { spend_auth_sig, recipient, value, + asset, rho, rseed, + rseed_split_note, fvk, witness, alpha, + split_flag, zip32_derivation, dummy_sk, proprietary, @@ -229,12 +277,9 @@ impl Output { .into_option() .ok_or(ParseError::InvalidExtractedNoteCommitment)?; - let encrypted_note = TransmittedNoteCiphertext:: { + let encrypted_note = PcztTransmittedNoteCiphertext { epk_bytes: ephemeral_key, - enc_ciphertext: ::NoteCiphertextBytes::from_slice( - enc_ciphertext.as_slice(), - ) - .ok_or(ParseError::InvalidEncCiphertext)?, + enc_ciphertext: enc_ciphertext.to_vec(), out_ciphertext: out_ciphertext .as_slice() .try_into() @@ -301,6 +346,8 @@ impl Zip32Derivation { pub enum ParseError { /// An invalid anchor was provided. InvalidAnchor, + /// An invalid `burn` was provided. + InvalidBurn, /// An invalid `bsk` was provided. InvalidBindingSignatureSigningKey, /// An invalid `dummy_sk` was provided. @@ -323,6 +370,8 @@ pub enum ParseError { InvalidRecipient, /// An invalid `rho` was provided. InvalidRho, + /// An invalid `SighashVersion` was provided. + InvalidSighashVersion, /// An invalid `alpha` was provided. InvalidSpendAuthRandomizer, /// An invalid `cv_net` was provided. diff --git a/src/pczt/prover.rs b/src/pczt/prover.rs index 5a103f9bd..32dc3ca36 100644 --- a/src/pczt/prover.rs +++ b/src/pczt/prover.rs @@ -5,15 +5,14 @@ use rand::{CryptoRng, RngCore}; use crate::{ builder::SpendInfo, - circuit::{Circuit, Instance, ProvingKey, Witnesses}, - note::{AssetBase, Rho}, - orchard_flavor::OrchardVanilla, + circuit::{Circuit, Instance, OrchardCircuit, ProvingKey, Witnesses}, + note::Rho, Note, Proof, }; impl super::Bundle { /// Adds a proof to this PCZT bundle. - pub fn create_proof( + pub fn create_proof( &mut self, pk: &ProvingKey, rng: R, @@ -35,27 +34,41 @@ impl super::Bundle { .clone() .ok_or(ProverError::MissingFullViewingKey)?; - let note = Note::from_parts( + let asset = action.spend.asset.ok_or(ProverError::MissingAsset)?; + + let mut note = Note::from_parts( action .spend .recipient .ok_or(ProverError::MissingRecipient)?, action.spend.value.ok_or(ProverError::MissingValue)?, - AssetBase::native(), + asset, action.spend.rho.ok_or(ProverError::MissingRho)?, action.spend.rseed.ok_or(ProverError::MissingRandomSeed)?, ) .into_option() .ok_or(ProverError::InvalidSpendNote)?; + if let Some(rseed) = action.spend.rseed_split_note { + note.set_rseed_split_note(rseed); + } + let merkle_path = action .spend .witness .clone() .ok_or(ProverError::MissingWitness)?; - let spend = SpendInfo::new(fvk, note, merkle_path, false) - .ok_or(ProverError::WrongFvkForNote)?; + let spend = SpendInfo::new( + fvk, + note, + merkle_path, + action + .spend + .split_flag + .ok_or(ProverError::MissingSplitFlag)?, + ) + .ok_or(ProverError::WrongFvkForNote)?; let output_note = Note::from_parts( action @@ -63,7 +76,7 @@ impl super::Bundle { .recipient .ok_or(ProverError::MissingRecipient)?, action.output.value.ok_or(ProverError::MissingValue)?, - AssetBase::native(), + asset, Rho::from_nf_old(action.spend.nullifier), action.output.rseed.ok_or(ProverError::MissingRandomSeed)?, ) @@ -76,9 +89,9 @@ impl super::Bundle { .ok_or(ProverError::MissingSpendAuthRandomizer)?; let rcv = action.rcv.ok_or(ProverError::MissingValueCommitTrapdoor)?; - Witnesses::from_action_context::(spend, output_note, alpha, rcv) + Witnesses::from_action_context::(spend, output_note, alpha, rcv) .ok_or(ProverError::RhoMismatch) - .map(|witnesses| Circuit:: { + .map(|witnesses| Circuit:: { witnesses, phantom: core::marker::PhantomData, }) @@ -128,10 +141,14 @@ pub enum ProverError { MissingSpendAuthRandomizer, /// The Prover role requires all `value` fields to be set. MissingValue, + /// The Prover role requires all `asset` fields to be set. + MissingAsset, /// The Prover role requires `rcv` to be set. MissingValueCommitTrapdoor, /// The Prover role requires `witness` to be set. MissingWitness, + /// The Prover role requires `split_flag` to be set. + MissingSplitFlag, /// 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. diff --git a/src/pczt/signer.rs b/src/pczt/signer.rs index 7f5f8028c..35ea2fe43 100644 --- a/src/pczt/signer.rs +++ b/src/pczt/signer.rs @@ -1,6 +1,10 @@ use rand::{CryptoRng, RngCore}; -use crate::{keys::SpendAuthorizingKey, primitives::redpallas}; +use crate::{ + keys::SpendAuthorizingKey, + orchard_sighash_versioning::{OrchardSighashVersion, VerSpendAuthSig}, + primitives::redpallas, +}; impl super::Action { /// Signs the Orchard spend with the given spend authorizing key. @@ -23,7 +27,10 @@ impl super::Action { let rk = redpallas::VerificationKey::from(&rsk); if self.spend.rk == rk { - self.spend.spend_auth_sig = Some(rsk.sign(rng, &sighash)); + self.spend.spend_auth_sig = Some(VerSpendAuthSig::new( + OrchardSighashVersion::V0, + rsk.sign(rng, &sighash), + )); Ok(()) } else { Err(SignerError::WrongSpendAuthorizingKey) diff --git a/src/pczt/tx_extractor.rs b/src/pczt/tx_extractor.rs index 057dc2831..ba2bcbc5d 100644 --- a/src/pczt/tx_extractor.rs +++ b/src/pczt/tx_extractor.rs @@ -4,9 +4,11 @@ use rand::{CryptoRng, RngCore}; use super::Action; use crate::{ bundle::{Authorization, Authorized, EffectsOnly}, - orchard_flavor::OrchardVanilla, orchard_sighash_versioning::{OrchardSighashVersion, VerBindingSig, VerSpendAuthSig}, - primitives::redpallas::{self, Binding, SpendAuth}, + primitives::{ + redpallas::{self, Binding}, + OrchardPrimitives, + }, Proof, }; @@ -16,9 +18,9 @@ impl super::Bundle { /// This is used by the Signer role to produce the transaction sighash. /// /// [regular `Bundle`]: crate::Bundle - pub fn extract_effects>( + pub fn extract_effects, P: OrchardPrimitives>( &self, - ) -> Result>, TxExtractorError> { + ) -> Result>, TxExtractorError> { self.to_tx_data(|_| Ok(()), |_| Ok(EffectsOnly)) } @@ -27,9 +29,9 @@ impl super::Bundle { /// This is used by the Transaction Extractor role to produce the final transaction. /// /// [regular `Bundle`]: crate::Bundle - pub fn extract>( + pub fn extract, P: OrchardPrimitives>( self, - ) -> Result>, TxExtractorError> { + ) -> Result>, TxExtractorError> { self.to_tx_data( |action| { action @@ -53,17 +55,18 @@ impl super::Bundle { } /// Converts this PCZT bundle into a regular bundle with the given authorizations. - fn to_tx_data( + fn to_tx_data( &self, action_auth: F, bundle_auth: G, - ) -> Result>, E> + ) -> Result>, E> where A: Authorization, E: From, F: Fn(&Action) -> Result<::SpendAuth, E>, G: FnOnce(&Self) -> Result, V: TryFrom, + P: OrchardPrimitives, { let actions = self .actions @@ -75,7 +78,7 @@ impl super::Bundle { action.spend.nullifier, action.spend.rk.clone(), action.output.cmx, - action.output.encrypted_note.clone(), + action.output.encrypted_note.clone().into(), action.cv_net.clone(), authorization, )) @@ -94,7 +97,7 @@ impl super::Bundle { actions, self.flags, value_balance, - vec![], //No burn in PCZT V1 + self.burn.clone(), self.anchor, authorization, )) @@ -125,10 +128,10 @@ pub struct Unbound { } impl Authorization for Unbound { - type SpendAuth = redpallas::Signature; + type SpendAuth = VerSpendAuthSig; } -impl crate::Bundle { +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`. @@ -136,15 +139,16 @@ impl crate::Bundle { self, sighash: [u8; 32], rng: R, - ) -> Option> { - if self - .actions() - .iter() - .all(|action| action.rk().verify(&sighash, action.authorization()).is_ok()) - { + ) -> Option> { + if self.actions().iter().all(|action| { + action + .rk() + .verify(&sighash, action.authorization().sig()) + .is_ok() + }) { Some(self.map_authorization( &mut (), - |_, _, a| VerSpendAuthSig::new(OrchardSighashVersion::V0, a), + |_, _, a| a, |_, Unbound { proof, bsk }| { Authorized::from_parts( proof, diff --git a/src/pczt/verify.rs b/src/pczt/verify.rs index c6a22b9e0..d4272f239 100644 --- a/src/pczt/verify.rs +++ b/src/pczt/verify.rs @@ -1,7 +1,7 @@ use crate::{ keys::{FullViewingKey, SpendValidatingKey}, - note::{AssetBase, ExtractedNoteCommitment, Rho}, - value::ValueCommitment, + note::{ExtractedNoteCommitment, Rho}, + value::{NoteValue, ValueCommitment}, Note, }; @@ -13,11 +13,24 @@ impl super::Action { /// - `output.value` /// - `rcv` pub fn verify_cv_net(&self) -> Result<(), VerifyError> { - let spend_value = self.spend().value.ok_or(VerifyError::MissingValue)?; + let spend_value = match self + .spend() + .split_flag + .ok_or(VerifyError::MissingSplitFlag)? + { + true => NoteValue::zero(), + false => self.spend().value.ok_or(VerifyError::MissingValue)?, + }; + + self.spend().value.ok_or(VerifyError::MissingValue)?; let output_value = self.output().value.ok_or(VerifyError::MissingValue)?; let rcv = self.rcv.ok_or(VerifyError::MissingValueCommitTrapdoor)?; - let cv_net = ValueCommitment::derive(spend_value - output_value, rcv, AssetBase::native()); + let cv_net = ValueCommitment::derive( + spend_value - output_value, + rcv, + self.spend.asset.ok_or(VerifyError::MissingAsset)?, + ); if cv_net.to_bytes() == self.cv_net.to_bytes() { Ok(()) } else { @@ -63,16 +76,20 @@ impl super::Spend { ) -> Result<(), VerifyError> { let fvk = self.fvk_for_validation(expected_fvk)?; - let note = Note::from_parts( + let mut note = Note::from_parts( self.recipient.ok_or(VerifyError::MissingRecipient)?, self.value.ok_or(VerifyError::MissingValue)?, - AssetBase::native(), + self.asset.ok_or(VerifyError::MissingAsset)?, self.rho.ok_or(VerifyError::MissingRho)?, self.rseed.ok_or(VerifyError::MissingRandomSeed)?, ) .into_option() .ok_or(VerifyError::InvalidSpendNote)?; + if let Some(rseed) = self.rseed_split_note { + note.set_rseed_split_note(rseed); + } + // We need both the note and the FVK to verify the nullifier; we have everything // needed to also verify that the correct FVK was provided (the nullifier check // itself only constrains `nk` within the FVK). @@ -125,7 +142,7 @@ impl super::Output { let note = Note::from_parts( self.recipient.ok_or(VerifyError::MissingRecipient)?, self.value.ok_or(VerifyError::MissingValue)?, - AssetBase::native(), + spend.asset.ok_or(VerifyError::MissingAsset)?, Rho::from_nf_old(spend.nullifier), self.rseed.ok_or(VerifyError::MissingRandomSeed)?, ) @@ -169,6 +186,10 @@ pub enum VerifyError { MissingSpendAuthRandomizer, /// Verification requires all `value` fields to be set. MissingValue, + /// Verification requires `asset` to be set. + MissingAsset, + /// Verification requires `split_flag` to be set. + MissingSplitFlag, /// `cv_net` verification requires `rcv` to be set. MissingValueCommitTrapdoor, /// The provided `fvk` does not own the spent note.