From 6f1aab69942f04ec6c81263d9e173fe06eb009e6 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Thu, 11 Jan 2024 23:57:57 -0600 Subject: [PATCH] feat: use Merlin's `TranscriptRng` for random number generation --- src/range_proof.rs | 84 ++++++++++------- src/transcripts.rs | 231 ++++++++++++++++++++++++++++++++++----------- 2 files changed, 226 insertions(+), 89 deletions(-) diff --git a/src/range_proof.rs b/src/range_proof.rs index 42ce126..436d038 100644 --- a/src/range_proof.rs +++ b/src/range_proof.rs @@ -19,7 +19,6 @@ use curve25519_dalek::{ traits::{Identity, IsIdentity, MultiscalarMul, VartimePrecomputedMultiscalarMul}, }; use itertools::{izip, Itertools}; -use merlin::Transcript; #[cfg(feature = "rand")] use rand::rngs::OsRng; use rand_core::CryptoRngCore; @@ -30,15 +29,11 @@ use crate::{ errors::ProofError, extended_mask::ExtendedMask, generators::pedersen_gens::ExtensionDegree, - protocols::{ - curve_point_protocol::CurvePointProtocol, - scalar_protocol::ScalarProtocol, - transcript_protocol::TranscriptProtocol, - }, + protocols::{curve_point_protocol::CurvePointProtocol, scalar_protocol::ScalarProtocol}, range_statement::RangeStatement, range_witness::RangeWitness, traits::{Compressable, Decompressable, FixedBytesRepr, Precomputable}, - transcripts, + transcripts::RangeProofTranscript, utils::generic::{nonce, split_at_checked}, }; @@ -282,17 +277,17 @@ where } } - // Start the transcript - let mut transcript = Transcript::new(transcript_label.as_bytes()); - transcript.domain_separator(b"Bulletproofs+", b"Range Proof"); - transcripts::transcript_initialize::

( - &mut transcript, + // Start a new transcript and generate the transcript RNG + let (mut transcript, mut transcript_rng) = RangeProofTranscript::

::new( + transcript_label, &statement.generators.h_base().compress(), statement.generators.g_bases_compressed(), bit_length, extension_degree, aggregation_factor, statement, + Some(witness), + rng, )?; // Set bit arrays @@ -327,7 +322,7 @@ where nonce(&seed_nonce, "alpha", None, Some(k))? } else { // Zero is allowed by the protocol, but excluded by the implementation to be unambiguous - Scalar::random_not_zero(rng) + Scalar::random_not_zero(&mut transcript_rng) }); } let a = statement.generators.precomp().vartime_mixed_multiscalar_mul( @@ -336,8 +331,9 @@ where statement.generators.g_bases().iter(), ); - // Get challenges - let (y, z) = transcripts::transcript_point_a_challenges_y_z(&mut transcript, &a.compress())?; + // Update transcript, get challenges, and update RNG + let (y, z) = transcript.challenges_y_z(&mut transcript_rng, rng, &a.compress())?; + let z_square = z * z; // Compute powers of the challenge @@ -420,7 +416,11 @@ where ) } else { // Zero is allowed by the protocol, but excluded by the implementation to be unambiguous - Zeroizing::new((0..extension_degree).map(|_| Scalar::random_not_zero(rng)).collect()) + Zeroizing::new( + (0..extension_degree) + .map(|_| Scalar::random_not_zero(&mut transcript_rng)) + .collect(), + ) }; let d_r = if let Some(seed_nonce) = statement.seed_nonce { Zeroizing::new( @@ -430,7 +430,11 @@ where ) } else { // Zero is allowed by the protocol, but excluded by the implementation to be unambiguous - Zeroizing::new((0..extension_degree).map(|_| Scalar::random_not_zero(rng)).collect()) + Zeroizing::new( + (0..extension_degree) + .map(|_| Scalar::random_not_zero(&mut transcript_rng)) + .collect(), + ) }; round += 1; @@ -460,9 +464,10 @@ where once(h_base).chain(g_base.iter()).chain(gi_base_lo).chain(hi_base_hi), )); - // Get the round challenge and associated values - let e = transcripts::transcript_points_l_r_challenge_e( - &mut transcript, + // Update transcript, get challenge, and update RNG + let e = transcript.challenge_round_e( + &mut transcript_rng, + rng, &li.last() .ok_or(ProofError::InvalidLength("Bad inner product vector length".to_string()))? .compress(), @@ -506,8 +511,8 @@ where // Random masks // Zero is allowed by the protocol, but excluded by the implementation to be unambiguous - let r = Zeroizing::new(Scalar::random_not_zero(rng)); - let s = Zeroizing::new(Scalar::random_not_zero(rng)); + let r = Zeroizing::new(Scalar::random_not_zero(&mut transcript_rng)); + let s = Zeroizing::new(Scalar::random_not_zero(&mut transcript_rng)); let d = if let Some(seed_nonce) = statement.seed_nonce { Zeroizing::new( (0..extension_degree) @@ -516,7 +521,11 @@ where ) } else { // Zero is allowed by the protocol, but excluded by the implementation to be unambiguous - Zeroizing::new((0..extension_degree).map(|_| Scalar::random_not_zero(rng)).collect()) + Zeroizing::new( + (0..extension_degree) + .map(|_| Scalar::random_not_zero(&mut transcript_rng)) + .collect(), + ) }; let eta = if let Some(seed_nonce) = statement.seed_nonce { Zeroizing::new( @@ -526,7 +535,11 @@ where ) } else { // Zero is allowed by the protocol, but excluded by the implementation to be unambiguous - Zeroizing::new((0..extension_degree).map(|_| Scalar::random_not_zero(rng)).collect()) + Zeroizing::new( + (0..extension_degree) + .map(|_| Scalar::random_not_zero(&mut transcript_rng)) + .collect(), + ) }; let mut a1 = @@ -539,7 +552,8 @@ where b += g_base * eta; } - let e = transcripts::transcript_points_a1_b_challenge_e(&mut transcript, &a1.compress(), &b.compress())?; + // Update transcript, get challenge, and update RNG + let e = transcript.challenge_final_e(&mut transcript_rng, rng, &a1.compress(), &b.compress())?; let e_square = e * e; let r1 = *r + a_li[0] * e; @@ -810,31 +824,31 @@ where return Err(ProofError::InvalidLength("Vector L/R length not adequate".to_string())); } - // Batch weight (may not be equal to a zero valued scalar) - this may not be zero ever - let weight = Scalar::random_not_zero(rng); - // Start the transcript - let mut transcript = Transcript::new((*transcript_label).as_bytes()); - transcript.domain_separator(b"Bulletproofs+", b"Range Proof"); - transcripts::transcript_initialize( - &mut transcript, + let (mut transcript, mut transcript_rng) = RangeProofTranscript::new( + transcript_label, &h_base_compressed, g_bases_compressed, bit_length, extension_degree, aggregation_factor, statement, + None, + rng, )?; // Reconstruct challenges - let (y, z) = transcripts::transcript_point_a_challenges_y_z(&mut transcript, &proof.a)?; + let (y, z) = transcript.challenges_y_z(&mut transcript_rng, rng, &proof.a)?; let challenges = proof .li .iter() .zip(proof.ri.iter()) - .map(|(l, r)| transcripts::transcript_points_l_r_challenge_e(&mut transcript, l, r)) + .map(|(l, r)| transcript.challenge_round_e(&mut transcript_rng, rng, l, r)) .collect::, ProofError>>()?; - let e = transcripts::transcript_points_a1_b_challenge_e(&mut transcript, &proof.a1, &proof.b)?; + let e = transcript.challenge_final_e(&mut transcript_rng, rng, &proof.a1, &proof.b)?; + + // Batch weight (may not be equal to a zero valued scalar) - this may not be zero ever + let weight = Scalar::random_not_zero(&mut transcript_rng); // Compute challenge inverses in a batch let mut challenges_inv = challenges.clone(); diff --git a/src/transcripts.rs b/src/transcripts.rs index 72a1f91..ffb2026 100644 --- a/src/transcripts.rs +++ b/src/transcripts.rs @@ -1,76 +1,199 @@ // Copyright 2022 The Tari Project // SPDX-License-Identifier: BSD-3-Clause +use core::mem::size_of; +use std::marker::PhantomData; + use curve25519_dalek::{scalar::Scalar, traits::IsIdentity}; -use merlin::Transcript; +use merlin::{Transcript, TranscriptRng}; +use rand_core::CryptoRngCore; +use zeroize::Zeroizing; use crate::{ errors::ProofError, protocols::transcript_protocol::TranscriptProtocol, range_statement::RangeStatement, + range_witness::RangeWitness, traits::{Compressable, FixedBytesRepr, Precomputable}, }; -// Helper function to construct the initial transcript -pub(crate) fn transcript_initialize

( - transcript: &mut Transcript, - h_base_compressed: &P::Compressed, - g_base_compressed: &[P::Compressed], - bit_length: usize, - extension_degree: usize, - aggregation_factor: usize, - statement: &RangeStatement

, -) -> Result<(), ProofError> +/// A wrapper around a Merlin transcript. +/// +/// This does the usual Fiat-Shamir operations: initialize a transcript, add proof messages, and get challenges. +/// +/// But it does more! +/// Following the design from [Merlin](https://merlin.cool/transcript/rng.html), it provides a random number generator. +/// It does this using the latest transcript state, (optional) secret data, and an external random number generator. +/// This helps to guard against failure of the external random number generator. +/// +/// When the prover initializes the wrapper, it includes the witness as the secret data. +/// The verifier doesn't have any secret data to include, so it passes `None` instead. +/// In either case, you get a `RangeProofTranscript` and a `TranscriptRng`. +/// +/// When the transcript is updated using the challenge functions, you must provide the `TranscriptRng`, which is also +/// updated. +/// +/// When randomness is needed, just use the `TranscriptRng`. +/// The prover uses this whenever it needs a random nonce. +/// The batch verifier uses this to generate weights. +pub(crate) struct RangeProofTranscript

+where + P: Compressable + Precomputable, + P::Compressed: FixedBytesRepr + IsIdentity, +{ + transcript: Transcript, + bytes: Option>>, + _phantom: PhantomData

, +} + +impl

RangeProofTranscript

where P: Compressable + Precomputable, P::Compressed: FixedBytesRepr + IsIdentity, { - transcript.validate_and_append_point(b"H", h_base_compressed)?; - for item in g_base_compressed { - transcript.validate_and_append_point(b"G", item)?; + /// Initialize a transcript. + /// + /// The prover should include its `witness` here; the verifier should pass `None`. + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + label: &'static str, + h_base_compressed: &P::Compressed, + g_base_compressed: &[P::Compressed], + bit_length: usize, + extension_degree: usize, + aggregation_factor: usize, + statement: &RangeStatement

, + witness: Option<&RangeWitness>, + external_rng: &mut R, + ) -> Result<(Self, TranscriptRng), ProofError> { + // Initialize the transcript with parameters and statement + let mut transcript = Transcript::new(label.as_bytes()); + transcript.domain_separator(b"Bulletproofs+", b"Range Proof"); + transcript.validate_and_append_point(b"H", h_base_compressed)?; + for item in g_base_compressed { + transcript.validate_and_append_point(b"G", item)?; + } + transcript.append_u64(b"N", bit_length as u64); + transcript.append_u64(b"T", extension_degree as u64); + transcript.append_u64(b"M", aggregation_factor as u64); + for item in &statement.commitments_compressed { + transcript.append_point(b"Ci", item); + } + for item in &statement.minimum_value_promises { + if let Some(minimum_value) = item { + transcript.append_u64(b"vi - minimum_value", *minimum_value); + } else { + transcript.append_u64(b"vi - minimum_value", 0); + } + } + + // Serialize the witness if provided + let bytes = if let Some(witness) = witness { + let size: usize = witness + .openings + .iter() + .map(|o| size_of::() + o.r.len() * size_of::()) + .sum(); + let mut witness_bytes = Zeroizing::new(Vec::::with_capacity(size)); + for opening in &witness.openings { + witness_bytes.extend(opening.v.to_le_bytes()); + for r in &opening.r { + witness_bytes.extend(r.as_bytes()); + } + } + + Some(witness_bytes) + } else { + None + }; + + // Set up the RNG + let transcript_rng = Self::build_rng(&transcript, bytes.as_ref(), external_rng); + + Ok(( + Self { + transcript, + bytes, + _phantom: PhantomData, + }, + transcript_rng, + )) + } + + // Construct the `y` and `z` challenges and update the RNG + pub(crate) fn challenges_y_z( + &mut self, + transcript_rng: &mut TranscriptRng, + external_rng: &mut R, + a: &P::Compressed, + ) -> Result<(Scalar, Scalar), ProofError> { + // Update the transcript + self.transcript.validate_and_append_point(b"A", a)?; + + // Update the RNG + *transcript_rng = Self::build_rng(&self.transcript, self.bytes.as_ref(), external_rng); + + // Return the challenges + Ok(( + self.transcript.challenge_scalar(b"y")?, + self.transcript.challenge_scalar(b"z")?, + )) + } + + /// Construct an inner-product round `e` challenge and update the RNG + pub(crate) fn challenge_round_e( + &mut self, + transcript_rng: &mut TranscriptRng, + external_rng: &mut R, + l: &P::Compressed, + r: &P::Compressed, + ) -> Result { + // Update the transcript + self.transcript.validate_and_append_point(b"L", l)?; + self.transcript.validate_and_append_point(b"R", r)?; + + // Update the RNG + *transcript_rng = Self::build_rng(&self.transcript, self.bytes.as_ref(), external_rng); + + // Return the challenge + self.transcript.challenge_scalar(b"e") } - transcript.append_u64(b"N", bit_length as u64); - transcript.append_u64(b"T", extension_degree as u64); - transcript.append_u64(b"M", aggregation_factor as u64); - for item in &statement.commitments_compressed { - transcript.append_point(b"Ci", item); + + /// Construct the final `e` challenge and update the RNG + pub(crate) fn challenge_final_e( + &mut self, + transcript_rng: &mut TranscriptRng, + external_rng: &mut R, + a1: &P::Compressed, + b: &P::Compressed, + ) -> Result { + // Update the transcript + self.transcript.validate_and_append_point(b"A1", a1)?; + self.transcript.validate_and_append_point(b"B", b)?; + + // Update the RNG + *transcript_rng = Self::build_rng(&self.transcript, self.bytes.as_ref(), external_rng); + + // Return the challenge + self.transcript.challenge_scalar(b"e") } - for item in &statement.minimum_value_promises { - if let Some(minimum_value) = item { - transcript.append_u64(b"vi - minimum_value", *minimum_value); + + /// Construct a random number generator from the current transcript state + /// + /// Internally, this builds the RNG using a clone of the transcript state, the secret bytes (if provided), and the + /// external RNG. + fn build_rng( + transcript: &Transcript, + bytes: Option<&Zeroizing>>, + external_rng: &mut R, + ) -> TranscriptRng { + if let Some(bytes) = bytes { + transcript + .build_rng() + .rekey_with_witness_bytes("witness".as_bytes(), bytes) + .finalize(external_rng) } else { - transcript.append_u64(b"vi - minimum_value", 0); + transcript.build_rng().finalize(external_rng) } } - Ok(()) -} -// Helper function to construct the y and z challenge scalars after points A -pub(crate) fn transcript_point_a_challenges_y_z( - transcript: &mut Transcript, - a: &P, -) -> Result<(Scalar, Scalar), ProofError> { - transcript.validate_and_append_point(b"A", a)?; - Ok((transcript.challenge_scalar(b"y")?, transcript.challenge_scalar(b"z")?)) -} - -/// Helper function to construct the e challenge scalar after points L and R -pub(crate) fn transcript_points_l_r_challenge_e( - transcript: &mut Transcript, - l: &P, - r: &P, -) -> Result { - transcript.validate_and_append_point(b"L", l)?; - transcript.validate_and_append_point(b"R", r)?; - transcript.challenge_scalar(b"e") -} - -/// Helper function to construct the e challenge scalar after points A1 and B -pub(crate) fn transcript_points_a1_b_challenge_e( - transcript: &mut Transcript, - a1: &P, - b: &P, -) -> Result { - transcript.validate_and_append_point(b"A1", a1)?; - transcript.validate_and_append_point(b"B", b)?; - transcript.challenge_scalar(b"e") }