diff --git a/src/lib.rs b/src/lib.rs index 6d465ea..b8e24e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,6 +122,8 @@ pub use proof::Proof; /// Triptych proof statements. pub mod statement; pub use statement::{InputSet, Statement}; +/// Triptych proof transcripts. +pub(crate) mod transcript; /// Various utility functionality. pub(crate) mod util; /// Triptych proof witnesses. diff --git a/src/parameters.rs b/src/parameters.rs index 5b59ca4..9b85670 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -41,6 +41,9 @@ pub enum ParameterError { } impl Parameters { + // Version identifier used for hashing + const VERSION: u64 = 0; + /// Generate new [`Parameters`] for Triptych proofs. /// /// The base `n > 1` and exponent `m > 1` define the size of verification key vectors, so it must be the case that @@ -110,6 +113,7 @@ impl Parameters { // Use `BLAKE3` for the transcript hash let mut hasher = Hasher::new(); hasher.update("Triptych Parameters".as_bytes()); + hasher.update(&Self::VERSION.to_le_bytes()); hasher.update(&n.to_le_bytes()); hasher.update(&m.to_le_bytes()); hasher.update(G.compress().as_bytes()); diff --git a/src/proof.rs b/src/proof.rs index 3be28f5..92eaf56 100644 --- a/src/proof.rs +++ b/src/proof.rs @@ -22,13 +22,11 @@ use zeroize::Zeroizing; use crate::{ gray::GrayIterator, statement::Statement, - util::{NullRng, OperationTiming}, + transcript::ProofTranscript, + util::{delta, NullRng, OperationTiming}, witness::Witness, }; -// Proof version flag -const VERSION: u64 = 0; - // Size of serialized proof elements in bytes const SERIALIZED_BYTES: usize = 32; @@ -66,38 +64,6 @@ pub enum ProofError { FailedVerification, } -/// Constant-time Kronecker delta function with scalar output. -fn delta(x: u32, y: u32) -> Scalar { - let mut result = Scalar::ZERO; - result.conditional_assign(&Scalar::ONE, x.ct_eq(&y)); - result -} - -/// Get nonzero powers of a challenge value from a transcript. -/// -/// If successful, returns powers of the challenge with exponents `[0, m]`. -/// If any power is zero, returns an error. -fn xi_powers(transcript: &mut Transcript, m: u32) -> Result, ProofError> { - // Get the verifier challenge using wide reduction - let mut xi_bytes = [0u8; 64]; - transcript.challenge_bytes("xi".as_bytes(), &mut xi_bytes); - let xi = Scalar::from_bytes_mod_order_wide(&xi_bytes); - - // Get powers of the challenge and confirm they are nonzero - let mut xi_powers = Vec::with_capacity((m as usize).checked_add(1).ok_or(ProofError::InvalidParameter)?); - let mut xi_power = Scalar::ONE; - for _ in 0..=m { - if xi_power == Scalar::ZERO { - return Err(ProofError::InvalidChallenge); - } - - xi_powers.push(xi_power); - xi_power *= xi; - } - - Ok(xi_powers) -} - impl Proof { /// Generate a Triptych [`Proof`]. /// @@ -217,26 +183,15 @@ impl Proof { return Err(ProofError::InvalidParameter); } - // Continue the transcript with domain separation - transcript.append_message("dom-sep".as_bytes(), "Triptych proof".as_bytes()); - transcript.append_u64("version".as_bytes(), VERSION); - transcript.append_message("params".as_bytes(), params.get_hash()); - transcript.append_message("M".as_bytes(), statement.get_input_set().get_hash()); - transcript.append_message("J".as_bytes(), J.compress().as_bytes()); - - // Construct a random number generator at the current transcript state - let mut transcript_rng = transcript - .build_rng() - .rekey_with_witness_bytes("l".as_bytes(), &l.to_le_bytes()) - .rekey_with_witness_bytes("r".as_bytes(), r.as_bytes()) - .finalize(rng); + // Set up the transcript + let mut transcript = ProofTranscript::new(transcript, statement, rng, Some(witness)); // Compute the `A` matrix commitment - let r_A = Scalar::random(&mut transcript_rng); + let r_A = Scalar::random(transcript.as_mut_rng()); let mut a = (0..params.get_m()) .map(|_| { (0..params.get_n()) - .map(|_| Scalar::random(&mut transcript_rng)) + .map(|_| Scalar::random(transcript.as_mut_rng())) .collect::>() }) .collect::>>(); @@ -248,7 +203,7 @@ impl Proof { .map_err(|_| ProofError::InvalidParameter)?; // Compute the `B` matrix commitment - let r_B = Scalar::random(&mut transcript_rng); + let r_B = Scalar::random(transcript.as_mut_rng()); let l_decomposed = match timing { OperationTiming::Constant => { GrayIterator::decompose(params.get_n(), params.get_m(), l).ok_or(ProofError::InvalidParameter)? @@ -269,7 +224,7 @@ impl Proof { // Compute the `C` matrix commitment let two = Scalar::from(2u32); - let r_C = Scalar::random(&mut transcript_rng); + let r_C = Scalar::random(transcript.as_mut_rng()); let a_sigma = (0..params.get_m()) .map(|j| { (0..params.get_n()) @@ -282,7 +237,7 @@ impl Proof { .map_err(|_| ProofError::InvalidParameter)?; // Compute the `D` matrix commitment - let r_D = Scalar::random(&mut transcript_rng); + let r_D = Scalar::random(transcript.as_mut_rng()); let a_square = (0..params.get_m()) .map(|j| { (0..params.get_n()) @@ -297,7 +252,7 @@ impl Proof { // Random masks let rho = Zeroizing::new( (0..params.get_m()) - .map(|_| Scalar::random(&mut transcript_rng)) + .map(|_| Scalar::random(transcript.as_mut_rng())) .collect::>(), ); @@ -365,20 +320,8 @@ impl Proof { // Compute `Y` vector let Y = rho.iter().map(|rho| rho * J).collect::>(); - // Update the transcript - transcript.append_message("A".as_bytes(), A.compress().as_bytes()); - transcript.append_message("B".as_bytes(), B.compress().as_bytes()); - transcript.append_message("C".as_bytes(), C.compress().as_bytes()); - transcript.append_message("D".as_bytes(), D.compress().as_bytes()); - for item in &X { - transcript.append_message("X".as_bytes(), item.compress().as_bytes()); - } - for item in &Y { - transcript.append_message("Y".as_bytes(), item.compress().as_bytes()); - } - - // Get challenge powers - let xi_powers = xi_powers(transcript, params.get_m())?; + // Run the Fiat-Shamir commitment phase to get the challenge powers + let xi_powers = transcript.commit(params, &A, &B, &C, &D, &X, &Y)?; // Compute the `f` matrix let f = (0..params.get_m()) @@ -543,47 +486,24 @@ impl Proof { // Set up a transcript generator for use in weighting let mut transcript_weights = Transcript::new("Triptych verifier weights".as_bytes()); + let mut null_rng = NullRng; + // Generate all verifier challenges let mut xi_powers_all = Vec::with_capacity(proofs.len()); for (statement, proof, transcript) in izip!(statements.iter(), proofs.iter(), transcripts.iter_mut()) { - // Generate the verifier challenge - transcript.append_message("dom-sep".as_bytes(), "Triptych proof".as_bytes()); - transcript.append_u64("version".as_bytes(), VERSION); - transcript.append_message("params".as_bytes(), params.get_hash()); - transcript.append_message("M".as_bytes(), statement.get_input_set().get_hash()); - transcript.append_message("J".as_bytes(), statement.get_J().compress().as_bytes()); - - transcript.append_message("A".as_bytes(), proof.A.compress().as_bytes()); - transcript.append_message("B".as_bytes(), proof.B.compress().as_bytes()); - transcript.append_message("C".as_bytes(), proof.C.compress().as_bytes()); - transcript.append_message("D".as_bytes(), proof.D.compress().as_bytes()); - for item in &proof.X { - transcript.append_message("X".as_bytes(), item.compress().as_bytes()); - } - for item in &proof.Y { - transcript.append_message("Y".as_bytes(), item.compress().as_bytes()); - } - - // Get challenge powers - let xi_powers = xi_powers(transcript, params.get_m())?; - xi_powers_all.push(xi_powers); + // Set up the transcript + let mut transcript = ProofTranscript::new(transcript, statement, &mut null_rng, None); - // Finish the transcript for pseudorandom number generation - for f_row in &proof.f { - for f in f_row { - transcript.append_message("f".as_bytes(), f.as_bytes()); - } - } - transcript.append_message("z_A".as_bytes(), proof.z_A.as_bytes()); - transcript.append_message("z_C".as_bytes(), proof.z_C.as_bytes()); - transcript.append_message("z".as_bytes(), proof.z.as_bytes()); - let mut transcript_rng = transcript.build_rng().finalize(&mut NullRng); + // Run the Fiat-Shamir commitment phase to get the challenge powers + xi_powers_all.push(transcript.commit(params, &proof.A, &proof.B, &proof.C, &proof.D, &proof.X, &proof.Y)?); + // Run the Fiat-Shamir response phase to get the transcript generator and weight + let mut transcript_rng = transcript.response(&proof.f, &proof.z_A, &proof.z_C, &proof.z); transcript_weights.append_u64("proof".as_bytes(), transcript_rng.as_rngcore().next_u64()); } // Finalize the weighting transcript into a pseudorandom number generator - let mut transcript_weights_rng = transcript_weights.build_rng().finalize(&mut NullRng); + let mut transcript_weights_rng = transcript_weights.build_rng().finalize(&mut null_rng); // Process each proof for (proof, xi_powers) in proofs.iter().zip(xi_powers_all.iter()) { diff --git a/src/statement.rs b/src/statement.rs index 658c3de..edb3186 100644 --- a/src/statement.rs +++ b/src/statement.rs @@ -21,12 +21,16 @@ pub struct InputSet { } impl InputSet { + // Version identifier used for hashing + const VERSION: u64 = 0; + /// Generate a new [`InputSet`] from a slice `M` of verification keys. #[allow(non_snake_case)] pub fn new(M: &[RistrettoPoint]) -> Self { // Use `BLAKE3` for the transcript hash let mut hasher = Hasher::new(); hasher.update("Triptych InputSet".as_bytes()); + hasher.update(&Self::VERSION.to_le_bytes()); for item in M { hasher.update(item.compress().as_bytes()); } diff --git a/src/transcript.rs b/src/transcript.rs new file mode 100644 index 0000000..b0e2fc0 --- /dev/null +++ b/src/transcript.rs @@ -0,0 +1,137 @@ +// Copyright (c) 2024, The Tari Project +// SPDX-License-Identifier: BSD-3-Clause + +use alloc::vec::Vec; + +use curve25519_dalek::{RistrettoPoint, Scalar}; +use merlin::{Transcript, TranscriptRng}; +use rand_core::CryptoRngCore; + +use crate::{proof::ProofError, Parameters, Statement, Witness}; + +// Version identifier +const VERSION: u64 = 0; + +// Domain separator +const DOMAIN: &str = "Triptych proof"; + +/// A Triptych proof transcript. +pub(crate) struct ProofTranscript<'a, R: CryptoRngCore> { + transcript: &'a mut Transcript, + witness: Option<&'a Witness>, + transcript_rng: TranscriptRng, + external_rng: &'a mut R, +} + +impl<'a, R: CryptoRngCore> ProofTranscript<'a, R> { + /// Initialize a transcript. + pub(crate) fn new( + transcript: &'a mut Transcript, + statement: &Statement, + external_rng: &'a mut R, + witness: Option<&'a Witness>, + ) -> Self { + // Update the transcript + transcript.append_message(b"dom-sep", DOMAIN.as_bytes()); + transcript.append_u64(b"version", VERSION); + transcript.append_message(b"params", statement.get_params().get_hash()); + transcript.append_message(b"M", statement.get_input_set().get_hash()); + transcript.append_message(b"J", statement.get_J().compress().as_bytes()); + + // Set up the transcript generator + let transcript_rng = Self::build_transcript_rng(transcript, witness, external_rng); + + Self { + transcript, + witness, + transcript_rng, + external_rng, + } + } + + /// Run the Fiat-Shamir commitment phase and produce challenge powers + #[allow(non_snake_case, clippy::too_many_arguments)] + pub(crate) fn commit( + &mut self, + params: &Parameters, + A: &RistrettoPoint, + B: &RistrettoPoint, + C: &RistrettoPoint, + D: &RistrettoPoint, + X: &Vec, + Y: &Vec, + ) -> Result, ProofError> { + let m = params.get_m() as usize; + + // Update the transcript + self.transcript.append_message(b"A", A.compress().as_bytes()); + self.transcript.append_message(b"B", B.compress().as_bytes()); + self.transcript.append_message(b"C", C.compress().as_bytes()); + self.transcript.append_message(b"D", D.compress().as_bytes()); + for X_item in X { + self.transcript.append_message(b"X", X_item.compress().as_bytes()); + } + for Y_item in Y { + self.transcript.append_message(b"Y", Y_item.compress().as_bytes()); + } + + // Update the transcript generator + self.transcript_rng = Self::build_transcript_rng(self.transcript, self.witness, self.external_rng); + + // Get the initial challenge using wide reduction + let mut xi_bytes = [0u8; 64]; + self.transcript.challenge_bytes("xi".as_bytes(), &mut xi_bytes); + let xi = Scalar::from_bytes_mod_order_wide(&xi_bytes); + + // Get powers of the challenge and confirm they are nonzero + let mut xi_powers = Vec::with_capacity(m.checked_add(1).ok_or(ProofError::InvalidParameter)?); + let mut xi_power = Scalar::ONE; + for _ in 0..=m { + if xi_power == Scalar::ZERO { + return Err(ProofError::InvalidChallenge); + } + + xi_powers.push(xi_power); + xi_power *= xi; + } + + Ok(xi_powers) + } + + /// Run the Fiat-Shamir response phase + #[allow(non_snake_case)] + pub(crate) fn response(mut self, f: &Vec>, z_A: &Scalar, z_C: &Scalar, z: &Scalar) -> TranscriptRng { + // Update the transcript + for f_row in f { + for f in f_row { + self.transcript.append_message(b"f", f.as_bytes()); + } + } + self.transcript.append_message(b"z_A", z_A.as_bytes()); + self.transcript.append_message(b"z_C", z_C.as_bytes()); + self.transcript.append_message(b"z", z.as_bytes()); + + // Update the transcript generator + self.transcript_rng = Self::build_transcript_rng(self.transcript, self.witness, self.external_rng); + + self.transcript_rng + } + + /// Get a mutable reference to the transcript generator + pub(crate) fn as_mut_rng(&mut self) -> &mut TranscriptRng { + &mut self.transcript_rng + } + + /// Build a random number generator from a transcript, optionally binding in witness data. + fn build_transcript_rng(transcript: &Transcript, witness: Option<&Witness>, external_rng: &mut R) -> TranscriptRng { + if let Some(witness) = witness { + transcript + .build_rng() + .rekey_with_witness_bytes(b"l", &witness.get_l().to_le_bytes()) + .rekey_with_witness_bytes(b"r", witness.get_r().as_bytes()) + .finalize(external_rng) + } else { + transcript.build_rng().finalize(external_rng) + } + } +} diff --git a/src/util.rs b/src/util.rs index a29b5a5..42999c3 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,11 +1,13 @@ // Copyright (c) 2024, The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use curve25519_dalek::Scalar; use rand_core::{ impls::{next_u32_via_fill, next_u64_via_fill}, CryptoRng, RngCore, }; +use subtle::{ConditionallySelectable, ConstantTimeEq}; use zeroize::Zeroize; /// Options for constant- or variable-time operations. @@ -17,6 +19,13 @@ pub(crate) enum OperationTiming { Variable, } +/// Constant-time Kronecker delta function with scalar output. +pub(crate) fn delta(x: u32, y: u32) -> Scalar { + let mut result = Scalar::ZERO; + result.conditional_assign(&Scalar::ONE, x.ct_eq(&y)); + result +} + /// A null random number generator that exists only for deterministic transcript-based weight generation. /// It only produces zero. /// This is DANGEROUS in general, and you almost certainly should not use it elsewhere!