From 231f81911628499a8877be57e66e60c09e55bdea Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 13 Dec 2024 00:13:20 +0000 Subject: [PATCH 1/2] Add methods for validating aspects of PCZT bundles --- src/pczt.rs | 3 + src/pczt/verify.rs | 190 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 src/pczt/verify.rs diff --git a/src/pczt.rs b/src/pczt.rs index 080abab..2bbeafc 100644 --- a/src/pczt.rs +++ b/src/pczt.rs @@ -21,6 +21,9 @@ use crate::{ mod parse; pub use parse::ParseError; +mod verify; +pub use verify::VerifyError; + mod io_finalizer; pub use io_finalizer::IoFinalizerError; diff --git a/src/pczt/verify.rs b/src/pczt/verify.rs new file mode 100644 index 0000000..2e5078f --- /dev/null +++ b/src/pczt/verify.rs @@ -0,0 +1,190 @@ +use crate::{keys::FullViewingKey, value::ValueCommitment, Note, ViewingKey}; + +impl super::Spend { + /// Verifies that the `cv` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `value` + /// - `rcv` + pub fn verify_cv(&self) -> Result<(), VerifyError> { + let value = self.value.ok_or(VerifyError::MissingValue)?; + let rcv = self + .rcv + .clone() + .ok_or(VerifyError::MissingValueCommitTrapdoor)?; + + let cv_net = ValueCommitment::derive(value, rcv); + if cv_net.to_bytes() == self.cv.to_bytes() { + Ok(()) + } else { + Err(VerifyError::InvalidValueCommitment) + } + } + + /// Returns the [`ViewingKey`] to use when validating this note. + /// + /// Handles dummy notes when the `value` field is set. + fn vk_for_validation( + &self, + expected_fvk: Option<&FullViewingKey>, + ) -> Result { + let vk = self + .proof_generation_key + .as_ref() + .map(|proof_generation_key| proof_generation_key.to_viewing_key()); + + match (expected_fvk, vk, self.value.as_ref()) { + // Dummy notes use random keys, which must be provided. + (_, Some(vk), Some(value)) if value.inner() == 0 => Ok(vk), + (_, None, Some(value)) if value.inner() == 0 => { + Err(VerifyError::MissingProofGenerationKey) + } + // If the `proof_generation_key` field has been pruned, assume the caller + // provided the correct FVK. + (Some(expected_fvk), None, _) => Ok(expected_fvk.vk.clone()), + // This is not a dummy note; if the FVK field is present, it must match. + (Some(expected_fvk), Some(vk), _) + if vk.ak == expected_fvk.vk.ak && vk.nk == expected_fvk.vk.nk => + { + Ok(vk) + } + (Some(_), Some(_), _) => Err(VerifyError::MismatchedFullViewingKey), + (None, Some(vk), _) => Ok(vk), + (None, None, _) => Err(VerifyError::MissingProofGenerationKey), + } + } + + /// Verifies that the `nullifier` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `recipient` + /// - `value` + /// - `rseed` + /// - `witness` + /// + /// The provided [`FullViewingKey`] is ignored if the spent note is a dummy note. + /// Otherwise, it will be checked against the `proof_generation_key` field (if set). + pub fn verify_nullifier( + &self, + expected_fvk: Option<&FullViewingKey>, + ) -> Result<(), VerifyError> { + let vk = self.vk_for_validation(expected_fvk)?; + + let note = Note::from_parts( + self.recipient.ok_or(VerifyError::MissingRecipient)?, + self.value.ok_or(VerifyError::MissingValue)?, + self.rseed.ok_or(VerifyError::MissingRandomSeed)?, + ); + + // We need both the note and the VK to verify the nullifier; we have everything + // needed to also verify that the correct VK was provided (the nullifier check + // itself only constrains `nk` within the VK). + if vk.to_payment_address(*note.recipient().diversifier()) != Some(note.recipient()) { + return Err(VerifyError::WrongFvkForNote); + } + + let merkle_path = self.witness().as_ref().ok_or(VerifyError::MissingWitness)?; + + if note.nf(&vk.nk, merkle_path.position().into()) == self.nullifier { + Ok(()) + } else { + Err(VerifyError::InvalidNullifier) + } + } + + /// Verifies that the `rk` field is consistent with the given FVK. + /// + /// Requires that the following optional fields are set: + /// - `alpha` + /// + /// The provided [`FullViewingKey`] is ignored if the spent note is a dummy note + /// (which can only be determined if the `value` field is set). Otherwise, it will be + /// checked against the `proof_generation_key` field (if set). + pub fn verify_rk(&self, expected_fvk: Option<&FullViewingKey>) -> Result<(), VerifyError> { + let vk = self.vk_for_validation(expected_fvk)?; + + let alpha = self + .alpha + .as_ref() + .ok_or(VerifyError::MissingSpendAuthRandomizer)?; + + if vk.ak.randomize(alpha) == self.rk { + Ok(()) + } else { + Err(VerifyError::InvalidRandomizedVerificationKey) + } + } +} + +impl super::Output { + /// Verifies that the `cv` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `value` + /// - `rcv` + pub fn verify_cv(&self) -> Result<(), VerifyError> { + let value = self.value.ok_or(VerifyError::MissingValue)?; + let rcv = self + .rcv + .clone() + .ok_or(VerifyError::MissingValueCommitTrapdoor)?; + + let cv_net = ValueCommitment::derive(value, rcv); + if cv_net.to_bytes() == self.cv.to_bytes() { + Ok(()) + } else { + Err(VerifyError::InvalidValueCommitment) + } + } + + /// Verifies that the `cmu` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `recipient` + /// - `value` + /// - `rseed` + pub fn verify_note_commitment(&self) -> Result<(), VerifyError> { + let note = Note::from_parts( + self.recipient.ok_or(VerifyError::MissingRecipient)?, + self.value.ok_or(VerifyError::MissingValue)?, + crate::Rseed::AfterZip212(self.rseed.ok_or(VerifyError::MissingRandomSeed)?), + ); + + if note.cmu() == self.cmu { + Ok(()) + } else { + Err(VerifyError::InvalidExtractedNoteCommitment) + } + } +} + +/// Errors that can occur while verifying a PCZT bundle. +#[derive(Debug)] +pub enum VerifyError { + /// The output note's components do not produce the expected `cmx`. + InvalidExtractedNoteCommitment, + /// The spent note's components do not produce the expected `nullifier`. + InvalidNullifier, + /// The Spend's FVK and `alpha` do not produce the expected `rk`. + InvalidRandomizedVerificationKey, + /// The action's `cv_net` does not match the provided note values and `rcv`. + InvalidValueCommitment, + /// The spend or output's `fvk` field does not match the provided FVK. + MismatchedFullViewingKey, + /// Dummy notes must have their `proof_generation_key` field set in order to be verified. + MissingProofGenerationKey, + /// `nullifier` verification requires `rseed` to be set. + MissingRandomSeed, + /// `nullifier` verification requires `recipient` to be set. + MissingRecipient, + /// `rk` verification requires `alpha` to be set. + MissingSpendAuthRandomizer, + /// Verification requires all `value` fields to be set. + MissingValue, + /// `cv_net` verification requires `rcv` to be set. + MissingValueCommitTrapdoor, + /// `nullifier` verification requires `witness` to be set. + MissingWitness, + /// The provided `fvk` does not own the spent note. + WrongFvkForNote, +} From 94ad2b356e9b495eae90a93e8b0c27d06924e79e Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 13 Dec 2024 07:50:04 +0000 Subject: [PATCH 2/2] Adjust cases in `pczt::Spend::vk_for_validation` Co-authored-by: Daira-Emma Hopwood --- src/pczt/verify.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pczt/verify.rs b/src/pczt/verify.rs index 2e5078f..704d00a 100644 --- a/src/pczt/verify.rs +++ b/src/pczt/verify.rs @@ -34,21 +34,15 @@ impl super::Spend { .map(|proof_generation_key| proof_generation_key.to_viewing_key()); match (expected_fvk, vk, self.value.as_ref()) { - // Dummy notes use random keys, which must be provided. - (_, Some(vk), Some(value)) if value.inner() == 0 => Ok(vk), - (_, None, Some(value)) if value.inner() == 0 => { - Err(VerifyError::MissingProofGenerationKey) - } - // If the `proof_generation_key` field has been pruned, assume the caller - // provided the correct FVK. - (Some(expected_fvk), None, _) => Ok(expected_fvk.vk.clone()), - // This is not a dummy note; if the FVK field is present, it must match. (Some(expected_fvk), Some(vk), _) if vk.ak == expected_fvk.vk.ak && vk.nk == expected_fvk.vk.nk => { Ok(vk) } + // `expected_fvk` is ignored if the spent note is a dummy note. + (Some(_), Some(vk), Some(value)) if value.inner() == 0 => Ok(vk), (Some(_), Some(_), _) => Err(VerifyError::MismatchedFullViewingKey), + (Some(expected_fvk), None, _) => Ok(expected_fvk.vk.clone()), (None, Some(vk), _) => Ok(vk), (None, None, _) => Err(VerifyError::MissingProofGenerationKey), }