Skip to content

Commit 938b348

Browse files
authored
r1cs: optional serialization for second-phase commitments with a version tag (#264)
If there were no second phase allocations, the proof is shorter by 3x32 bytes: second-phase commitments are treated as identity points. If there were no randomization callbacks, the proof is domain-separated from one with randomization callbacks. Note: it is possible to use randomized constraints, yet still perform no allocations in the second phase and receive a shorter proof. In this case the two-phase domain separator is still applied. Closes #242 Closes #258 Prior iteration: #258, diff: oleg/optional-2phase...oleg/optional-2phase-tagged
1 parent fe71f06 commit 938b348

File tree

4 files changed

+153
-64
lines changed

4 files changed

+153
-64
lines changed

src/r1cs/proof.rs

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
44
use curve25519_dalek::ristretto::CompressedRistretto;
55
use curve25519_dalek::scalar::Scalar;
6+
use curve25519_dalek::traits::{Identity, IsIdentity};
67

78
use errors::R1CSError;
89
use inner_product_proof::InnerProductProof;
@@ -11,6 +12,9 @@ use util;
1112
use serde::de::Visitor;
1213
use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
1314

15+
const ONE_PHASE_COMMITMENTS: u8 = 0;
16+
const TWO_PHASE_COMMITMENTS: u8 = 1;
17+
1418
/// A proof of some statement specified by a
1519
/// [`ConstraintSystem`](::r1cs::ConstraintSystem).
1620
///
@@ -64,25 +68,34 @@ pub struct R1CSProof {
6468
}
6569

6670
impl R1CSProof {
67-
/// Serializes the proof into a byte array of \\(16 + 2k\\) 32-byte elements,
71+
/// Serializes the proof into a byte array of 1 version byte + \\((13 or 16) + 2k\\) 32-byte elements,
6872
/// where \\(k=\lceil \log_2(n) \rceil\\) and \\(n\\) is the number of multiplication gates.
6973
///
7074
/// # Layout
7175
///
7276
/// The layout of the r1cs proof encoding is:
73-
///
74-
/// * eleven compressed Ristretto points \\(A_{I1},A_{O1},S_1,A_{I2},A_{O2},S_2,T_1,...,T_6\\),
77+
/// * 1 version byte indicating whether the proof contains second-phase commitments or not,
78+
/// * 8 or 11 compressed Ristretto points \\(A_{I1},A_{O1},S_1,(A_{I2},A_{O2},S_2),T_1,...,T_6\\)
79+
/// (\\(A_{I2},A_{O2},S_2\\) are skipped if there were no multipliers added in the randomized phase),
7580
/// * three scalars \\(t_x, \tilde{t}_x, \tilde{e}\\),
7681
/// * \\(k\\) pairs of compressed Ristretto points \\(L_0,R_0\dots,L_{k-1},R_{k-1}\\),
7782
/// * two scalars \\(a, b\\).
7883
pub fn to_bytes(&self) -> Vec<u8> {
7984
let mut buf = Vec::with_capacity(self.serialized_size());
80-
buf.extend_from_slice(self.A_I1.as_bytes());
81-
buf.extend_from_slice(self.A_O1.as_bytes());
82-
buf.extend_from_slice(self.S1.as_bytes());
83-
buf.extend_from_slice(self.A_I2.as_bytes());
84-
buf.extend_from_slice(self.A_O2.as_bytes());
85-
buf.extend_from_slice(self.S2.as_bytes());
85+
if self.missing_phase2_commitments() {
86+
buf.push(ONE_PHASE_COMMITMENTS);
87+
buf.extend_from_slice(self.A_I1.as_bytes());
88+
buf.extend_from_slice(self.A_O1.as_bytes());
89+
buf.extend_from_slice(self.S1.as_bytes());
90+
} else {
91+
buf.push(TWO_PHASE_COMMITMENTS);
92+
buf.extend_from_slice(self.A_I1.as_bytes());
93+
buf.extend_from_slice(self.A_O1.as_bytes());
94+
buf.extend_from_slice(self.S1.as_bytes());
95+
buf.extend_from_slice(self.A_I2.as_bytes());
96+
buf.extend_from_slice(self.A_O2.as_bytes());
97+
buf.extend_from_slice(self.S2.as_bytes());
98+
}
8699
buf.extend_from_slice(self.T_1.as_bytes());
87100
buf.extend_from_slice(self.T_3.as_bytes());
88101
buf.extend_from_slice(self.T_4.as_bytes());
@@ -98,18 +111,40 @@ impl R1CSProof {
98111

99112
/// Returns the size in bytes required to serialize the `R1CSProof`.
100113
pub fn serialized_size(&self) -> usize {
101-
// 14 elements + the ipp
102-
14 * 32 + self.ipp_proof.serialized_size()
114+
// version tag + (11 or 14) elements + the ipp
115+
let elements = if self.missing_phase2_commitments() {
116+
11
117+
} else {
118+
14
119+
};
120+
1 + elements * 32 + self.ipp_proof.serialized_size()
121+
}
122+
123+
fn missing_phase2_commitments(&self) -> bool {
124+
self.A_I2.is_identity() && self.A_O2.is_identity() && self.S2.is_identity()
103125
}
104126

105127
/// Deserializes the proof from a byte slice.
106128
///
107129
/// Returns an error if the byte slice cannot be parsed into a `R1CSProof`.
108-
pub fn from_bytes(mut slice: &[u8]) -> Result<R1CSProof, R1CSError> {
130+
pub fn from_bytes(slice: &[u8]) -> Result<R1CSProof, R1CSError> {
131+
if slice.len() < 1 {
132+
return Err(R1CSError::FormatError);
133+
}
134+
let version = slice[0];
135+
let mut slice = &slice[1..];
136+
109137
if slice.len() % 32 != 0 {
110138
return Err(R1CSError::FormatError);
111139
}
112-
if slice.len() < 14 * 32 {
140+
141+
let minlength = match version {
142+
ONE_PHASE_COMMITMENTS => 11 * 32,
143+
TWO_PHASE_COMMITMENTS => 14 * 32,
144+
_ => return Err(R1CSError::FormatError),
145+
};
146+
147+
if slice.len() < minlength {
113148
return Err(R1CSError::FormatError);
114149
}
115150

@@ -125,9 +160,19 @@ impl R1CSProof {
125160
let A_I1 = CompressedRistretto(read32!());
126161
let A_O1 = CompressedRistretto(read32!());
127162
let S1 = CompressedRistretto(read32!());
128-
let A_I2 = CompressedRistretto(read32!());
129-
let A_O2 = CompressedRistretto(read32!());
130-
let S2 = CompressedRistretto(read32!());
163+
let (A_I2, A_O2, S2) = if version == ONE_PHASE_COMMITMENTS {
164+
(
165+
CompressedRistretto::identity(),
166+
CompressedRistretto::identity(),
167+
CompressedRistretto::identity(),
168+
)
169+
} else {
170+
(
171+
CompressedRistretto(read32!()),
172+
CompressedRistretto(read32!()),
173+
CompressedRistretto(read32!()),
174+
)
175+
};
131176
let T_1 = CompressedRistretto(read32!());
132177
let T_3 = CompressedRistretto(read32!());
133178
let T_4 = CompressedRistretto(read32!());

src/r1cs/prover.rs

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use clear_on_drop::clear::Clear;
44
use core::mem;
55
use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint};
66
use curve25519_dalek::scalar::Scalar;
7-
use curve25519_dalek::traits::MultiscalarMul;
7+
use curve25519_dalek::traits::{Identity, MultiscalarMul};
88
use merlin::Transcript;
99

1010
use super::{ConstraintSystem, LinearCombination, R1CSProof, RandomizedConstraintSystem, Variable};
@@ -347,15 +347,21 @@ impl<'t, 'g> Prover<'t, 'g> {
347347
// Clear the pending multiplier (if any) because it was committed into A_L/A_R/S.
348348
self.pending_multiplier = None;
349349

350-
// Note: the wrapper could've used &mut instead of ownership,
351-
// but specifying lifetimes for boxed closures is not going to be nice,
352-
// so we move the self into wrapper and then move it back out afterwards.
353-
let mut callbacks = mem::replace(&mut self.deferred_constraints, Vec::new());
354-
let mut wrapped_self = RandomizingProver { prover: self };
355-
for callback in callbacks.drain(..) {
356-
callback(&mut wrapped_self)?;
350+
if self.deferred_constraints.len() == 0 {
351+
self.transcript.r1cs_1phase_domain_sep();
352+
Ok(self)
353+
} else {
354+
self.transcript.r1cs_2phase_domain_sep();
355+
// Note: the wrapper could've used &mut instead of ownership,
356+
// but specifying lifetimes for boxed closures is not going to be nice,
357+
// so we move the self into wrapper and then move it back out afterwards.
358+
let mut callbacks = mem::replace(&mut self.deferred_constraints, Vec::new());
359+
let mut wrapped_self = RandomizingProver { prover: self };
360+
for callback in callbacks.drain(..) {
361+
callback(&mut wrapped_self)?;
362+
}
363+
Ok(wrapped_self.prover)
357364
}
358-
Ok(wrapped_self.prover)
359365
}
360366

361367
/// Consume this `ConstraintSystem` to produce a proof.
@@ -461,41 +467,61 @@ impl<'t, 'g> Prover<'t, 'g> {
461467

462468
// Commit to the second-phase low-level witness variables
463469

464-
let i_blinding2 = Scalar::random(&mut rng);
465-
let o_blinding2 = Scalar::random(&mut rng);
466-
let s_blinding2 = Scalar::random(&mut rng);
470+
let has_2nd_phase_commitments = n2 > 0;
471+
472+
let (i_blinding2, o_blinding2, s_blinding2) = if has_2nd_phase_commitments {
473+
(
474+
Scalar::random(&mut rng),
475+
Scalar::random(&mut rng),
476+
Scalar::random(&mut rng),
477+
)
478+
} else {
479+
(Scalar::zero(), Scalar::zero(), Scalar::zero())
480+
};
467481

468482
let mut s_L2: Vec<Scalar> = (0..n2).map(|_| Scalar::random(&mut rng)).collect();
469483
let mut s_R2: Vec<Scalar> = (0..n2).map(|_| Scalar::random(&mut rng)).collect();
470484

471-
// A_I = <a_L, G> + <a_R, H> + i_blinding * B_blinding
472-
let A_I2 = RistrettoPoint::multiscalar_mul(
473-
iter::once(&i_blinding2)
474-
.chain(self.a_L.iter().skip(n1))
475-
.chain(self.a_R.iter().skip(n1)),
476-
iter::once(&self.pc_gens.B_blinding)
477-
.chain(gens.G(n).skip(n1))
478-
.chain(gens.H(n).skip(n1)),
479-
)
480-
.compress();
481-
482-
// A_O = <a_O, G> + o_blinding * B_blinding
483-
let A_O2 = RistrettoPoint::multiscalar_mul(
484-
iter::once(&o_blinding2).chain(self.a_O.iter().skip(n1)),
485-
iter::once(&self.pc_gens.B_blinding).chain(gens.G(n).skip(n1)),
486-
)
487-
.compress();
488-
489-
// S = <s_L, G> + <s_R, H> + s_blinding * B_blinding
490-
let S2 = RistrettoPoint::multiscalar_mul(
491-
iter::once(&s_blinding2)
492-
.chain(s_L2.iter())
493-
.chain(s_R2.iter()),
494-
iter::once(&self.pc_gens.B_blinding)
495-
.chain(gens.G(n).skip(n1))
496-
.chain(gens.H(n).skip(n1)),
497-
)
498-
.compress();
485+
let (A_I2, A_O2, S2) = if has_2nd_phase_commitments {
486+
(
487+
// A_I = <a_L, G> + <a_R, H> + i_blinding * B_blinding
488+
RistrettoPoint::multiscalar_mul(
489+
iter::once(&i_blinding2)
490+
.chain(self.a_L.iter().skip(n1))
491+
.chain(self.a_R.iter().skip(n1)),
492+
iter::once(&self.pc_gens.B_blinding)
493+
.chain(gens.G(n).skip(n1))
494+
.chain(gens.H(n).skip(n1)),
495+
)
496+
.compress(),
497+
// A_O = <a_O, G> + o_blinding * B_blinding
498+
RistrettoPoint::multiscalar_mul(
499+
iter::once(&o_blinding2).chain(self.a_O.iter().skip(n1)),
500+
iter::once(&self.pc_gens.B_blinding).chain(gens.G(n).skip(n1)),
501+
)
502+
.compress(),
503+
// S = <s_L, G> + <s_R, H> + s_blinding * B_blinding
504+
RistrettoPoint::multiscalar_mul(
505+
iter::once(&s_blinding2)
506+
.chain(s_L2.iter())
507+
.chain(s_R2.iter()),
508+
iter::once(&self.pc_gens.B_blinding)
509+
.chain(gens.G(n).skip(n1))
510+
.chain(gens.H(n).skip(n1)),
511+
)
512+
.compress(),
513+
)
514+
} else {
515+
// Since we are using zero blinding factors and
516+
// there are no variables to commit,
517+
// the commitments _must_ be identity points,
518+
// so we can hardcode them saving 3 mults+compressions.
519+
(
520+
CompressedRistretto::identity(),
521+
CompressedRistretto::identity(),
522+
CompressedRistretto::identity(),
523+
)
524+
};
499525

500526
self.transcript.commit_point(b"A_I2", &A_I2);
501527
self.transcript.commit_point(b"A_O2", &A_O2);

src/r1cs/verifier.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -291,15 +291,21 @@ impl<'t> Verifier<'t> {
291291
// Clear the pending multiplier (if any) because it was committed into A_L/A_R/S.
292292
self.pending_multiplier = None;
293293

294-
// Note: the wrapper could've used &mut instead of ownership,
295-
// but specifying lifetimes for boxed closures is not going to be nice,
296-
// so we move the self into wrapper and then move it back out afterwards.
297-
let mut callbacks = mem::replace(&mut self.deferred_constraints, Vec::new());
298-
let mut wrapped_self = RandomizingVerifier { verifier: self };
299-
for callback in callbacks.drain(..) {
300-
callback(&mut wrapped_self)?;
294+
if self.deferred_constraints.len() == 0 {
295+
self.transcript.r1cs_1phase_domain_sep();
296+
Ok(self)
297+
} else {
298+
self.transcript.r1cs_2phase_domain_sep();
299+
// Note: the wrapper could've used &mut instead of ownership,
300+
// but specifying lifetimes for boxed closures is not going to be nice,
301+
// so we move the self into wrapper and then move it back out afterwards.
302+
let mut callbacks = mem::replace(&mut self.deferred_constraints, Vec::new());
303+
let mut wrapped_self = RandomizingVerifier { verifier: self };
304+
for callback in callbacks.drain(..) {
305+
callback(&mut wrapped_self)?;
306+
}
307+
Ok(wrapped_self.verifier)
301308
}
302-
Ok(wrapped_self.verifier)
303309
}
304310

305311
/// Consume this `VerifierCS` and attempt to verify the supplied `proof`.

src/transcript.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ pub trait TranscriptProtocol {
1212
fn innerproduct_domain_sep(&mut self, n: u64);
1313
/// Commit a domain separator for a constraint system.
1414
fn r1cs_domain_sep(&mut self);
15+
/// Commit a domain separator for a CS without randomized constraints.
16+
fn r1cs_1phase_domain_sep(&mut self);
17+
/// Commit a domain separator for a CS with randomized constraints.
18+
fn r1cs_2phase_domain_sep(&mut self);
1519
/// Commit a `scalar` with the given `label`.
1620
fn commit_scalar(&mut self, label: &'static [u8], scalar: &Scalar);
1721
/// Commit a `point` with the given `label`.
@@ -42,6 +46,14 @@ impl TranscriptProtocol for Transcript {
4246
self.commit_bytes(b"dom-sep", b"r1cs v1");
4347
}
4448

49+
fn r1cs_1phase_domain_sep(&mut self) {
50+
self.commit_bytes(b"dom-sep", b"r1cs-1phase");
51+
}
52+
53+
fn r1cs_2phase_domain_sep(&mut self) {
54+
self.commit_bytes(b"dom-sep", b"r1cs-2phase");
55+
}
56+
4557
fn commit_scalar(&mut self, label: &'static [u8], scalar: &Scalar) {
4658
self.commit_bytes(label, scalar.as_bytes());
4759
}

0 commit comments

Comments
 (0)