Skip to content

Commit c455a66

Browse files
Refactor transcripting (#64)
This PR refactors Fiat-Shamir transcript functionality. It creates a new `ProofTranscript` wrapper around an existing Merlin transcript. This allows us to more cleanly unify the prover and verifier's operations. The design also better handles challenge power generation and the transcript random number generator used for both prover nonces and verifier weights. Because it also adds a version identifier to input set and parameter hashes, existing proofs will not verify. BREAKING CHANGE: Updates how internal hashing is performed, so existing proofs will not verify.
1 parent c7d7924 commit c455a66

File tree

6 files changed

+177
-101
lines changed

6 files changed

+177
-101
lines changed

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ pub use proof::Proof;
122122
/// Triptych proof statements.
123123
pub mod statement;
124124
pub use statement::{InputSet, Statement};
125+
/// Triptych proof transcripts.
126+
pub(crate) mod transcript;
125127
/// Various utility functionality.
126128
pub(crate) mod util;
127129
/// Triptych proof witnesses.

src/parameters.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ pub enum ParameterError {
4141
}
4242

4343
impl Parameters {
44+
// Version identifier used for hashing
45+
const VERSION: u64 = 0;
46+
4447
/// Generate new [`Parameters`] for Triptych proofs.
4548
///
4649
/// 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 {
110113
// Use `BLAKE3` for the transcript hash
111114
let mut hasher = Hasher::new();
112115
hasher.update("Triptych Parameters".as_bytes());
116+
hasher.update(&Self::VERSION.to_le_bytes());
113117
hasher.update(&n.to_le_bytes());
114118
hasher.update(&m.to_le_bytes());
115119
hasher.update(G.compress().as_bytes());

src/proof.rs

Lines changed: 21 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,11 @@ use zeroize::Zeroizing;
2222
use crate::{
2323
gray::GrayIterator,
2424
statement::Statement,
25-
util::{NullRng, OperationTiming},
25+
transcript::ProofTranscript,
26+
util::{delta, NullRng, OperationTiming},
2627
witness::Witness,
2728
};
2829

29-
// Proof version flag
30-
const VERSION: u64 = 0;
31-
3230
// Size of serialized proof elements in bytes
3331
const SERIALIZED_BYTES: usize = 32;
3432

@@ -66,38 +64,6 @@ pub enum ProofError {
6664
FailedVerification,
6765
}
6866

69-
/// Constant-time Kronecker delta function with scalar output.
70-
fn delta(x: u32, y: u32) -> Scalar {
71-
let mut result = Scalar::ZERO;
72-
result.conditional_assign(&Scalar::ONE, x.ct_eq(&y));
73-
result
74-
}
75-
76-
/// Get nonzero powers of a challenge value from a transcript.
77-
///
78-
/// If successful, returns powers of the challenge with exponents `[0, m]`.
79-
/// If any power is zero, returns an error.
80-
fn xi_powers(transcript: &mut Transcript, m: u32) -> Result<Vec<Scalar>, ProofError> {
81-
// Get the verifier challenge using wide reduction
82-
let mut xi_bytes = [0u8; 64];
83-
transcript.challenge_bytes("xi".as_bytes(), &mut xi_bytes);
84-
let xi = Scalar::from_bytes_mod_order_wide(&xi_bytes);
85-
86-
// Get powers of the challenge and confirm they are nonzero
87-
let mut xi_powers = Vec::with_capacity((m as usize).checked_add(1).ok_or(ProofError::InvalidParameter)?);
88-
let mut xi_power = Scalar::ONE;
89-
for _ in 0..=m {
90-
if xi_power == Scalar::ZERO {
91-
return Err(ProofError::InvalidChallenge);
92-
}
93-
94-
xi_powers.push(xi_power);
95-
xi_power *= xi;
96-
}
97-
98-
Ok(xi_powers)
99-
}
100-
10167
impl Proof {
10268
/// Generate a Triptych [`Proof`].
10369
///
@@ -217,26 +183,15 @@ impl Proof {
217183
return Err(ProofError::InvalidParameter);
218184
}
219185

220-
// Continue the transcript with domain separation
221-
transcript.append_message("dom-sep".as_bytes(), "Triptych proof".as_bytes());
222-
transcript.append_u64("version".as_bytes(), VERSION);
223-
transcript.append_message("params".as_bytes(), params.get_hash());
224-
transcript.append_message("M".as_bytes(), statement.get_input_set().get_hash());
225-
transcript.append_message("J".as_bytes(), J.compress().as_bytes());
226-
227-
// Construct a random number generator at the current transcript state
228-
let mut transcript_rng = transcript
229-
.build_rng()
230-
.rekey_with_witness_bytes("l".as_bytes(), &l.to_le_bytes())
231-
.rekey_with_witness_bytes("r".as_bytes(), r.as_bytes())
232-
.finalize(rng);
186+
// Set up the transcript
187+
let mut transcript = ProofTranscript::new(transcript, statement, rng, Some(witness));
233188

234189
// Compute the `A` matrix commitment
235-
let r_A = Scalar::random(&mut transcript_rng);
190+
let r_A = Scalar::random(transcript.as_mut_rng());
236191
let mut a = (0..params.get_m())
237192
.map(|_| {
238193
(0..params.get_n())
239-
.map(|_| Scalar::random(&mut transcript_rng))
194+
.map(|_| Scalar::random(transcript.as_mut_rng()))
240195
.collect::<Vec<Scalar>>()
241196
})
242197
.collect::<Vec<Vec<Scalar>>>();
@@ -248,7 +203,7 @@ impl Proof {
248203
.map_err(|_| ProofError::InvalidParameter)?;
249204

250205
// Compute the `B` matrix commitment
251-
let r_B = Scalar::random(&mut transcript_rng);
206+
let r_B = Scalar::random(transcript.as_mut_rng());
252207
let l_decomposed = match timing {
253208
OperationTiming::Constant => {
254209
GrayIterator::decompose(params.get_n(), params.get_m(), l).ok_or(ProofError::InvalidParameter)?
@@ -269,7 +224,7 @@ impl Proof {
269224

270225
// Compute the `C` matrix commitment
271226
let two = Scalar::from(2u32);
272-
let r_C = Scalar::random(&mut transcript_rng);
227+
let r_C = Scalar::random(transcript.as_mut_rng());
273228
let a_sigma = (0..params.get_m())
274229
.map(|j| {
275230
(0..params.get_n())
@@ -282,7 +237,7 @@ impl Proof {
282237
.map_err(|_| ProofError::InvalidParameter)?;
283238

284239
// Compute the `D` matrix commitment
285-
let r_D = Scalar::random(&mut transcript_rng);
240+
let r_D = Scalar::random(transcript.as_mut_rng());
286241
let a_square = (0..params.get_m())
287242
.map(|j| {
288243
(0..params.get_n())
@@ -297,7 +252,7 @@ impl Proof {
297252
// Random masks
298253
let rho = Zeroizing::new(
299254
(0..params.get_m())
300-
.map(|_| Scalar::random(&mut transcript_rng))
255+
.map(|_| Scalar::random(transcript.as_mut_rng()))
301256
.collect::<Vec<Scalar>>(),
302257
);
303258

@@ -365,20 +320,8 @@ impl Proof {
365320
// Compute `Y` vector
366321
let Y = rho.iter().map(|rho| rho * J).collect::<Vec<RistrettoPoint>>();
367322

368-
// Update the transcript
369-
transcript.append_message("A".as_bytes(), A.compress().as_bytes());
370-
transcript.append_message("B".as_bytes(), B.compress().as_bytes());
371-
transcript.append_message("C".as_bytes(), C.compress().as_bytes());
372-
transcript.append_message("D".as_bytes(), D.compress().as_bytes());
373-
for item in &X {
374-
transcript.append_message("X".as_bytes(), item.compress().as_bytes());
375-
}
376-
for item in &Y {
377-
transcript.append_message("Y".as_bytes(), item.compress().as_bytes());
378-
}
379-
380-
// Get challenge powers
381-
let xi_powers = xi_powers(transcript, params.get_m())?;
323+
// Run the Fiat-Shamir commitment phase to get the challenge powers
324+
let xi_powers = transcript.commit(params, &A, &B, &C, &D, &X, &Y)?;
382325

383326
// Compute the `f` matrix
384327
let f = (0..params.get_m())
@@ -543,47 +486,24 @@ impl Proof {
543486
// Set up a transcript generator for use in weighting
544487
let mut transcript_weights = Transcript::new("Triptych verifier weights".as_bytes());
545488

489+
let mut null_rng = NullRng;
490+
546491
// Generate all verifier challenges
547492
let mut xi_powers_all = Vec::with_capacity(proofs.len());
548493
for (statement, proof, transcript) in izip!(statements.iter(), proofs.iter(), transcripts.iter_mut()) {
549-
// Generate the verifier challenge
550-
transcript.append_message("dom-sep".as_bytes(), "Triptych proof".as_bytes());
551-
transcript.append_u64("version".as_bytes(), VERSION);
552-
transcript.append_message("params".as_bytes(), params.get_hash());
553-
transcript.append_message("M".as_bytes(), statement.get_input_set().get_hash());
554-
transcript.append_message("J".as_bytes(), statement.get_J().compress().as_bytes());
555-
556-
transcript.append_message("A".as_bytes(), proof.A.compress().as_bytes());
557-
transcript.append_message("B".as_bytes(), proof.B.compress().as_bytes());
558-
transcript.append_message("C".as_bytes(), proof.C.compress().as_bytes());
559-
transcript.append_message("D".as_bytes(), proof.D.compress().as_bytes());
560-
for item in &proof.X {
561-
transcript.append_message("X".as_bytes(), item.compress().as_bytes());
562-
}
563-
for item in &proof.Y {
564-
transcript.append_message("Y".as_bytes(), item.compress().as_bytes());
565-
}
566-
567-
// Get challenge powers
568-
let xi_powers = xi_powers(transcript, params.get_m())?;
569-
xi_powers_all.push(xi_powers);
494+
// Set up the transcript
495+
let mut transcript = ProofTranscript::new(transcript, statement, &mut null_rng, None);
570496

571-
// Finish the transcript for pseudorandom number generation
572-
for f_row in &proof.f {
573-
for f in f_row {
574-
transcript.append_message("f".as_bytes(), f.as_bytes());
575-
}
576-
}
577-
transcript.append_message("z_A".as_bytes(), proof.z_A.as_bytes());
578-
transcript.append_message("z_C".as_bytes(), proof.z_C.as_bytes());
579-
transcript.append_message("z".as_bytes(), proof.z.as_bytes());
580-
let mut transcript_rng = transcript.build_rng().finalize(&mut NullRng);
497+
// Run the Fiat-Shamir commitment phase to get the challenge powers
498+
xi_powers_all.push(transcript.commit(params, &proof.A, &proof.B, &proof.C, &proof.D, &proof.X, &proof.Y)?);
581499

500+
// Run the Fiat-Shamir response phase to get the transcript generator and weight
501+
let mut transcript_rng = transcript.response(&proof.f, &proof.z_A, &proof.z_C, &proof.z);
582502
transcript_weights.append_u64("proof".as_bytes(), transcript_rng.as_rngcore().next_u64());
583503
}
584504

585505
// Finalize the weighting transcript into a pseudorandom number generator
586-
let mut transcript_weights_rng = transcript_weights.build_rng().finalize(&mut NullRng);
506+
let mut transcript_weights_rng = transcript_weights.build_rng().finalize(&mut null_rng);
587507

588508
// Process each proof
589509
for (proof, xi_powers) in proofs.iter().zip(xi_powers_all.iter()) {

src/statement.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ pub struct InputSet {
2121
}
2222

2323
impl InputSet {
24+
// Version identifier used for hashing
25+
const VERSION: u64 = 0;
26+
2427
/// Generate a new [`InputSet`] from a slice `M` of verification keys.
2528
#[allow(non_snake_case)]
2629
pub fn new(M: &[RistrettoPoint]) -> Self {
2730
// Use `BLAKE3` for the transcript hash
2831
let mut hasher = Hasher::new();
2932
hasher.update("Triptych InputSet".as_bytes());
33+
hasher.update(&Self::VERSION.to_le_bytes());
3034
for item in M {
3135
hasher.update(item.compress().as_bytes());
3236
}

src/transcript.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) 2024, The Tari Project
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
use alloc::vec::Vec;
5+
6+
use curve25519_dalek::{RistrettoPoint, Scalar};
7+
use merlin::{Transcript, TranscriptRng};
8+
use rand_core::CryptoRngCore;
9+
10+
use crate::{proof::ProofError, Parameters, Statement, Witness};
11+
12+
// Version identifier
13+
const VERSION: u64 = 0;
14+
15+
// Domain separator
16+
const DOMAIN: &str = "Triptych proof";
17+
18+
/// A Triptych proof transcript.
19+
pub(crate) struct ProofTranscript<'a, R: CryptoRngCore> {
20+
transcript: &'a mut Transcript,
21+
witness: Option<&'a Witness>,
22+
transcript_rng: TranscriptRng,
23+
external_rng: &'a mut R,
24+
}
25+
26+
impl<'a, R: CryptoRngCore> ProofTranscript<'a, R> {
27+
/// Initialize a transcript.
28+
pub(crate) fn new(
29+
transcript: &'a mut Transcript,
30+
statement: &Statement,
31+
external_rng: &'a mut R,
32+
witness: Option<&'a Witness>,
33+
) -> Self {
34+
// Update the transcript
35+
transcript.append_message(b"dom-sep", DOMAIN.as_bytes());
36+
transcript.append_u64(b"version", VERSION);
37+
transcript.append_message(b"params", statement.get_params().get_hash());
38+
transcript.append_message(b"M", statement.get_input_set().get_hash());
39+
transcript.append_message(b"J", statement.get_J().compress().as_bytes());
40+
41+
// Set up the transcript generator
42+
let transcript_rng = Self::build_transcript_rng(transcript, witness, external_rng);
43+
44+
Self {
45+
transcript,
46+
witness,
47+
transcript_rng,
48+
external_rng,
49+
}
50+
}
51+
52+
/// Run the Fiat-Shamir commitment phase and produce challenge powers
53+
#[allow(non_snake_case, clippy::too_many_arguments)]
54+
pub(crate) fn commit(
55+
&mut self,
56+
params: &Parameters,
57+
A: &RistrettoPoint,
58+
B: &RistrettoPoint,
59+
C: &RistrettoPoint,
60+
D: &RistrettoPoint,
61+
X: &Vec<RistrettoPoint>,
62+
Y: &Vec<RistrettoPoint>,
63+
) -> Result<Vec<Scalar>, ProofError> {
64+
let m = params.get_m() as usize;
65+
66+
// Update the transcript
67+
self.transcript.append_message(b"A", A.compress().as_bytes());
68+
self.transcript.append_message(b"B", B.compress().as_bytes());
69+
self.transcript.append_message(b"C", C.compress().as_bytes());
70+
self.transcript.append_message(b"D", D.compress().as_bytes());
71+
for X_item in X {
72+
self.transcript.append_message(b"X", X_item.compress().as_bytes());
73+
}
74+
for Y_item in Y {
75+
self.transcript.append_message(b"Y", Y_item.compress().as_bytes());
76+
}
77+
78+
// Update the transcript generator
79+
self.transcript_rng = Self::build_transcript_rng(self.transcript, self.witness, self.external_rng);
80+
81+
// Get the initial challenge using wide reduction
82+
let mut xi_bytes = [0u8; 64];
83+
self.transcript.challenge_bytes("xi".as_bytes(), &mut xi_bytes);
84+
let xi = Scalar::from_bytes_mod_order_wide(&xi_bytes);
85+
86+
// Get powers of the challenge and confirm they are nonzero
87+
let mut xi_powers = Vec::with_capacity(m.checked_add(1).ok_or(ProofError::InvalidParameter)?);
88+
let mut xi_power = Scalar::ONE;
89+
for _ in 0..=m {
90+
if xi_power == Scalar::ZERO {
91+
return Err(ProofError::InvalidChallenge);
92+
}
93+
94+
xi_powers.push(xi_power);
95+
xi_power *= xi;
96+
}
97+
98+
Ok(xi_powers)
99+
}
100+
101+
/// Run the Fiat-Shamir response phase
102+
#[allow(non_snake_case)]
103+
pub(crate) fn response(mut self, f: &Vec<Vec<Scalar>>, z_A: &Scalar, z_C: &Scalar, z: &Scalar) -> TranscriptRng {
104+
// Update the transcript
105+
for f_row in f {
106+
for f in f_row {
107+
self.transcript.append_message(b"f", f.as_bytes());
108+
}
109+
}
110+
self.transcript.append_message(b"z_A", z_A.as_bytes());
111+
self.transcript.append_message(b"z_C", z_C.as_bytes());
112+
self.transcript.append_message(b"z", z.as_bytes());
113+
114+
// Update the transcript generator
115+
self.transcript_rng = Self::build_transcript_rng(self.transcript, self.witness, self.external_rng);
116+
117+
self.transcript_rng
118+
}
119+
120+
/// Get a mutable reference to the transcript generator
121+
pub(crate) fn as_mut_rng(&mut self) -> &mut TranscriptRng {
122+
&mut self.transcript_rng
123+
}
124+
125+
/// Build a random number generator from a transcript, optionally binding in witness data.
126+
fn build_transcript_rng(transcript: &Transcript, witness: Option<&Witness>, external_rng: &mut R) -> TranscriptRng {
127+
if let Some(witness) = witness {
128+
transcript
129+
.build_rng()
130+
.rekey_with_witness_bytes(b"l", &witness.get_l().to_le_bytes())
131+
.rekey_with_witness_bytes(b"r", witness.get_r().as_bytes())
132+
.finalize(external_rng)
133+
} else {
134+
transcript.build_rng().finalize(external_rng)
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)