Skip to content

Commit

Permalink
Refactor transcripting (tari-project#64)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
AaronFeickert authored Feb 20, 2024
1 parent c7d7924 commit c455a66
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 101 deletions.
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions src/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down
122 changes: 21 additions & 101 deletions src/proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Vec<Scalar>, 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`].
///
Expand Down Expand Up @@ -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::<Vec<Scalar>>()
})
.collect::<Vec<Vec<Scalar>>>();
Expand All @@ -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)?
Expand All @@ -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())
Expand All @@ -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())
Expand All @@ -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::<Vec<Scalar>>(),
);

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

// 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())
Expand Down Expand Up @@ -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()) {
Expand Down
4 changes: 4 additions & 0 deletions src/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
137 changes: 137 additions & 0 deletions src/transcript.rs
Original file line number Diff line number Diff line change
@@ -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<RistrettoPoint>,
Y: &Vec<RistrettoPoint>,
) -> Result<Vec<Scalar>, 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<Vec<Scalar>>, 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)
}
}
}
Loading

0 comments on commit c455a66

Please sign in to comment.