From 3d951b4201a63f3c07cba8b179dd9abde142cf33 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Thu, 12 Dec 2024 13:31:06 +0000 Subject: [PATCH 1/3] Add methods for validating aspects of PCZT bundles --- src/pczt.rs | 3 + src/pczt/verify.rs | 176 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 src/pczt/verify.rs diff --git a/src/pczt.rs b/src/pczt.rs index 7e079d5b4..ccdde32fb 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 000000000..5ab0b1801 --- /dev/null +++ b/src/pczt/verify.rs @@ -0,0 +1,176 @@ +use crate::{ + keys::{FullViewingKey, SpendValidatingKey}, + note::{ExtractedNoteCommitment, Rho}, + value::ValueCommitment, + Note, +}; + +impl super::Action { + /// Verifies that the `cv_net` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `spend.value` + /// - `output.value` + /// - `rcv` + pub fn verify_cv_net(&self) -> Result<(), VerifyError> { + let spend_value = self.spend().value.ok_or(VerifyError::MissingValue)?; + let output_value = self.output().value.ok_or(VerifyError::MissingValue)?; + let rcv = self + .rcv + .clone() + .ok_or(VerifyError::MissingValueCommitTrapdoor)?; + + let cv_net = ValueCommitment::derive(spend_value - output_value, rcv); + if cv_net.to_bytes() == self.cv_net.to_bytes() { + Ok(()) + } else { + Err(VerifyError::InvalidValueCommitment) + } + } +} + +impl super::Spend { + /// Returns the [`FullViewingKey`] to use when validating this note. + /// + /// Handles dummy notes when the `value` field is set. + fn fvk_for_validation<'a>( + &'a self, + expected_fvk: Option<&'a FullViewingKey>, + ) -> Result<&'a FullViewingKey, VerifyError> { + match (expected_fvk, self.fvk.as_ref(), self.value.as_ref()) { + // Dummy notes use random FVKs, which must be provided. + (_, Some(fvk), Some(value)) if value.inner() == 0 => Ok(fvk), + (_, None, Some(value)) if value.inner() == 0 => Err(VerifyError::MissingFullViewingKey), + // If the FVK field has been pruned, assume the caller provided the correct FVK. + (Some(expected_fvk), None, _) => Ok(expected_fvk), + // This is not a dummy note; if the FVK field is present, it must match. + (Some(expected_fvk), Some(fvk), _) if fvk == expected_fvk => Ok(fvk), + (Some(_), Some(_), _) => Err(VerifyError::MismatchedFullViewingKey), + (None, Some(fvk), _) => Ok(fvk), + (None, None, _) => Err(VerifyError::MissingFullViewingKey), + } + } + + /// Verifies that the `nullifier` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `recipient` + /// - `value` + /// - `rho` + /// - `rseed` + /// + /// The provided [`FullViewingKey`] is ignored if the spent note is a dummy note. + /// Otherwise, it will be checked against the `fvk` field (if set). + pub fn verify_nullifier( + &self, + expected_fvk: Option<&FullViewingKey>, + ) -> Result<(), VerifyError> { + let fvk = self.fvk_for_validation(expected_fvk)?; + + let note = Note::from_parts( + self.recipient.ok_or(VerifyError::MissingRecipient)?, + self.value.ok_or(VerifyError::MissingValue)?, + self.rho.ok_or(VerifyError::MissingRho)?, + self.rseed.ok_or(VerifyError::MissingRandomSeed)?, + ) + .into_option() + .ok_or(VerifyError::InvalidSpendNote)?; + + // 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). + fvk.scope_for_address(¬e.recipient()) + .ok_or(VerifyError::WrongFvkForNote)?; + + if note.nullifier(fvk) == 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 `fvk` field (if set). + pub fn verify_rk(&self, expected_fvk: Option<&FullViewingKey>) -> Result<(), VerifyError> { + let fvk = self.fvk_for_validation(expected_fvk)?; + + let ak = SpendValidatingKey::from(fvk.clone()); + + let alpha = self + .alpha + .as_ref() + .ok_or(VerifyError::MissingSpendAuthRandomizer)?; + + if ak.randomize(alpha) == self.rk { + Ok(()) + } else { + Err(VerifyError::InvalidRandomizedVerificationKey) + } + } +} + +impl super::Output { + /// Verifies that the `cmx` field is consistent with the note fields. + /// + /// Requires that the following optional fields are set: + /// - `recipient` + /// - `value` + /// - `rseed` + pub fn verify_note_commitment(&self, spend: &super::Spend) -> Result<(), VerifyError> { + let note = Note::from_parts( + self.recipient.ok_or(VerifyError::MissingRecipient)?, + self.value.ok_or(VerifyError::MissingValue)?, + Rho::from_nf_old(spend.nullifier), + self.rseed.ok_or(VerifyError::MissingRandomSeed)?, + ) + .into_option() + .ok_or(VerifyError::InvalidOutputNote)?; + + if ExtractedNoteCommitment::from(note.commitment()) == self.cmx { + 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 output note's components do not produce a valid note commitment. + InvalidOutputNote, + /// The Spend's FVK and `alpha` do not produce the expected `rk`. + InvalidRandomizedVerificationKey, + /// The spent note's components do not produce a valid note commitment. + InvalidSpendNote, + /// 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 `fvk` field set in order to be verified. + MissingFullViewingKey, + /// `nullifier` verification requires `rseed` to be set. + MissingRandomSeed, + /// `nullifier` verification requires `recipient` to be set. + MissingRecipient, + /// `nullifier` verification requires `rho` to be set. + MissingRho, + /// `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, + /// The provided `fvk` does not own the spent note. + WrongFvkForNote, +} From 3a5e66f992a717778c459741e75a48cc0e264864 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 13 Dec 2024 07:48:00 +0000 Subject: [PATCH 2/3] Adjust cases in `pczt::Spend::fvk_for_validation` Co-authored-by: Daira-Emma Hopwood --- src/pczt/verify.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pczt/verify.rs b/src/pczt/verify.rs index 5ab0b1801..721a93e6d 100644 --- a/src/pczt/verify.rs +++ b/src/pczt/verify.rs @@ -38,14 +38,11 @@ impl super::Spend { expected_fvk: Option<&'a FullViewingKey>, ) -> Result<&'a FullViewingKey, VerifyError> { match (expected_fvk, self.fvk.as_ref(), self.value.as_ref()) { - // Dummy notes use random FVKs, which must be provided. - (_, Some(fvk), Some(value)) if value.inner() == 0 => Ok(fvk), - (_, None, Some(value)) if value.inner() == 0 => Err(VerifyError::MissingFullViewingKey), - // If the FVK field has been pruned, assume the caller provided the correct FVK. - (Some(expected_fvk), None, _) => Ok(expected_fvk), - // This is not a dummy note; if the FVK field is present, it must match. (Some(expected_fvk), Some(fvk), _) if fvk == expected_fvk => Ok(fvk), + // `expected_fvk` is ignored if the spent note is a dummy note. + (Some(_), Some(fvk), Some(value)) if value.inner() == 0 => Ok(fvk), (Some(_), Some(_), _) => Err(VerifyError::MismatchedFullViewingKey), + (Some(expected_fvk), None, _) => Ok(expected_fvk), (None, Some(fvk), _) => Ok(fvk), (None, None, _) => Err(VerifyError::MissingFullViewingKey), } From 9b99350b38f309676a85bb2779bb9173b42b398e Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 13 Dec 2024 07:48:38 +0000 Subject: [PATCH 3/3] Clarify spend to use in `pczt::Output::verify_note_commitment` Co-authored-by: Daira-Emma Hopwood --- src/pczt/verify.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pczt/verify.rs b/src/pczt/verify.rs index 721a93e6d..1b16608ee 100644 --- a/src/pczt/verify.rs +++ b/src/pczt/verify.rs @@ -119,6 +119,8 @@ impl super::Output { /// - `recipient` /// - `value` /// - `rseed` + /// + /// `spend` must be the Spend from the same Orchard action. pub fn verify_note_commitment(&self, spend: &super::Spend) -> Result<(), VerifyError> { let note = Note::from_parts( self.recipient.ok_or(VerifyError::MissingRecipient)?,