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:: (
- 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 ,
+}
+
+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 ,
+ 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::