Skip to content

Commit 461d4a5

Browse files
authored
r1cs: single-variable allocation API + simpler API for assignments (#256)
This adds efficient and clean API for allocating one variable at a time. Here is how it simplifies the code in Spacesuit and ZkVM: stellar/slingshot#211 Plus, the assignments are provided simply as `Option<Scalar>` and `Option<(Scalar,Scalar)>` to make code look nicer. The overhead in verifier of checking for `None` and not doing `Option::map` or something in the gadget code should be negligible. Closes #206
1 parent 64ce140 commit 461d4a5

File tree

4 files changed

+183
-22
lines changed

4 files changed

+183
-22
lines changed

src/r1cs/constraint_system.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,29 @@ pub trait ConstraintSystem {
3939
right: LinearCombination,
4040
) -> (Variable, Variable, Variable);
4141

42+
/// Allocate a single variable.
43+
///
44+
/// This either allocates a new multiplier and returns its `left` variable,
45+
/// or returns a `right` variable of a multiplier previously allocated by this method.
46+
/// The output of a multiplier is assigned on a even call, when `right` is assigned.
47+
///
48+
/// When CS is committed at the end of the first or second phase, the half-assigned multiplier
49+
/// has the `right` assigned to zero and all its variables committed.
50+
///
51+
/// Returns unconstrained `Variable` for use in further constraints.
52+
fn allocate(&mut self, assignment: Option<Scalar>) -> Result<Variable, R1CSError>;
53+
4254
/// Allocate variables `left`, `right`, and `out`
4355
/// with the implicit constraint that
4456
/// ```text
4557
/// left * right = out
4658
/// ```
4759
///
4860
/// Returns `(left, right, out)` for use in further constraints.
49-
fn allocate<F>(&mut self, assign_fn: F) -> Result<(Variable, Variable, Variable), R1CSError>
50-
where
51-
F: FnOnce() -> Result<(Scalar, Scalar, Scalar), R1CSError>;
61+
fn allocate_multiplier(
62+
&mut self,
63+
input_assignments: Option<(Scalar, Scalar)>,
64+
) -> Result<(Variable, Variable, Variable), R1CSError>;
5265

5366
/// Enforce the explicit constraint that
5467
/// ```text

src/r1cs/prover.rs

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ pub struct Prover<'a, 'b> {
4343
/// This list holds closures that will be called in the second phase of the protocol,
4444
/// when non-randomized variables are committed.
4545
deferred_constraints: Vec<Box<Fn(&mut RandomizingProver<'a, 'b>) -> Result<(), R1CSError>>>,
46+
47+
/// Index of a pending multiplier that's not fully assigned yet.
48+
pending_multiplier: Option<usize>,
4649
}
4750

4851
/// Prover in the randomizing phase.
@@ -111,11 +114,33 @@ impl<'a, 'b> ConstraintSystem for Prover<'a, 'b> {
111114
(l_var, r_var, o_var)
112115
}
113116

114-
fn allocate<F>(&mut self, assign_fn: F) -> Result<(Variable, Variable, Variable), R1CSError>
115-
where
116-
F: FnOnce() -> Result<(Scalar, Scalar, Scalar), R1CSError>,
117-
{
118-
let (l, r, o) = assign_fn()?;
117+
fn allocate(&mut self, assignment: Option<Scalar>) -> Result<Variable, R1CSError> {
118+
let scalar = assignment.ok_or(R1CSError::MissingAssignment)?;
119+
120+
match self.pending_multiplier {
121+
None => {
122+
let i = self.a_L.len();
123+
self.pending_multiplier = Some(i);
124+
self.a_L.push(scalar);
125+
self.a_R.push(Scalar::zero());
126+
self.a_O.push(Scalar::zero());
127+
Ok(Variable::MultiplierLeft(i))
128+
}
129+
Some(i) => {
130+
self.pending_multiplier = None;
131+
self.a_R[i] = scalar;
132+
self.a_O[i] = self.a_L[i] * self.a_R[i];
133+
Ok(Variable::MultiplierRight(i))
134+
}
135+
}
136+
}
137+
138+
fn allocate_multiplier(
139+
&mut self,
140+
input_assignments: Option<(Scalar, Scalar)>,
141+
) -> Result<(Variable, Variable, Variable), R1CSError> {
142+
let (l, r) = input_assignments.ok_or(R1CSError::MissingAssignment)?;
143+
let o = l * r;
119144

120145
// Create variables for l,r,o ...
121146
let l_var = Variable::MultiplierLeft(self.a_L.len());
@@ -155,11 +180,15 @@ impl<'a, 'b> ConstraintSystem for RandomizingProver<'a, 'b> {
155180
self.prover.multiply(left, right)
156181
}
157182

158-
fn allocate<F>(&mut self, assign_fn: F) -> Result<(Variable, Variable, Variable), R1CSError>
159-
where
160-
F: FnOnce() -> Result<(Scalar, Scalar, Scalar), R1CSError>,
161-
{
162-
self.prover.allocate(assign_fn)
183+
fn allocate(&mut self, assignment: Option<Scalar>) -> Result<Variable, R1CSError> {
184+
self.prover.allocate(assignment)
185+
}
186+
187+
fn allocate_multiplier(
188+
&mut self,
189+
input_assignments: Option<(Scalar, Scalar)>,
190+
) -> Result<(Variable, Variable, Variable), R1CSError> {
191+
self.prover.allocate_multiplier(input_assignments)
163192
}
164193

165194
fn constrain(&mut self, lc: LinearCombination) {
@@ -219,6 +248,7 @@ impl<'a, 'b> Prover<'a, 'b> {
219248
a_R: Vec::new(),
220249
a_O: Vec::new(),
221250
deferred_constraints: Vec::new(),
251+
pending_multiplier: None,
222252
}
223253
}
224254

@@ -320,6 +350,9 @@ impl<'a, 'b> Prover<'a, 'b> {
320350
/// Calls all remembered callbacks with an API that
321351
/// allows generating challenge scalars.
322352
fn create_randomized_constraints(mut self) -> Result<Self, R1CSError> {
353+
// Clear the pending multiplier (if any) because it was committed into A_L/A_R/S.
354+
self.pending_multiplier = None;
355+
323356
// Note: the wrapper could've used &mut instead of ownership,
324357
// but specifying lifetimes for boxed closures is not going to be nice,
325358
// so we move the self into wrapper and then move it back out afterwards.

src/r1cs/verifier.rs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ pub struct Verifier<'a, 'b> {
4242
/// After that, the option will flip to None and additional calls to `randomize_constraints`
4343
/// will invoke closures immediately.
4444
deferred_constraints: Vec<Box<Fn(&mut RandomizingVerifier<'a, 'b>) -> Result<(), R1CSError>>>,
45+
46+
/// Index of a pending multiplier that's not fully assigned yet.
47+
pending_multiplier: Option<usize>,
4548
}
4649

4750
/// Verifier in the randomizing phase.
@@ -80,10 +83,25 @@ impl<'a, 'b> ConstraintSystem for Verifier<'a, 'b> {
8083
(l_var, r_var, o_var)
8184
}
8285

83-
fn allocate<F>(&mut self, _: F) -> Result<(Variable, Variable, Variable), R1CSError>
84-
where
85-
F: FnOnce() -> Result<(Scalar, Scalar, Scalar), R1CSError>,
86-
{
86+
fn allocate(&mut self, _: Option<Scalar>) -> Result<Variable, R1CSError> {
87+
match self.pending_multiplier {
88+
None => {
89+
let i = self.num_vars;
90+
self.num_vars += 1;
91+
self.pending_multiplier = Some(i);
92+
Ok(Variable::MultiplierLeft(i))
93+
}
94+
Some(i) => {
95+
self.pending_multiplier = None;
96+
Ok(Variable::MultiplierRight(i))
97+
}
98+
}
99+
}
100+
101+
fn allocate_multiplier(
102+
&mut self,
103+
_: Option<(Scalar, Scalar)>,
104+
) -> Result<(Variable, Variable, Variable), R1CSError> {
87105
let var = self.num_vars;
88106
self.num_vars += 1;
89107

@@ -122,11 +140,15 @@ impl<'a, 'b> ConstraintSystem for RandomizingVerifier<'a, 'b> {
122140
self.verifier.multiply(left, right)
123141
}
124142

125-
fn allocate<F>(&mut self, assign_fn: F) -> Result<(Variable, Variable, Variable), R1CSError>
126-
where
127-
F: FnOnce() -> Result<(Scalar, Scalar, Scalar), R1CSError>,
128-
{
129-
self.verifier.allocate(assign_fn)
143+
fn allocate(&mut self, assignment: Option<Scalar>) -> Result<Variable, R1CSError> {
144+
self.verifier.allocate(assignment)
145+
}
146+
147+
fn allocate_multiplier(
148+
&mut self,
149+
input_assignments: Option<(Scalar, Scalar)>,
150+
) -> Result<(Variable, Variable, Variable), R1CSError> {
151+
self.verifier.allocate_multiplier(input_assignments)
130152
}
131153

132154
fn constrain(&mut self, lc: LinearCombination) {
@@ -194,6 +216,7 @@ impl<'a, 'b> Verifier<'a, 'b> {
194216
V: Vec::new(),
195217
constraints: Vec::new(),
196218
deferred_constraints: Vec::new(),
219+
pending_multiplier: None,
197220
}
198221
}
199222

@@ -279,6 +302,9 @@ impl<'a, 'b> Verifier<'a, 'b> {
279302
/// Calls all remembered callbacks with an API that
280303
/// allows generating challenge scalars.
281304
fn create_randomized_constraints(mut self) -> Result<Self, R1CSError> {
305+
// Clear the pending multiplier (if any) because it was committed into A_L/A_R/S.
306+
self.pending_multiplier = None;
307+
282308
// Note: the wrapper could've used &mut instead of ownership,
283309
// but specifying lifetimes for boxed closures is not going to be nice,
284310
// so we move the self into wrapper and then move it back out afterwards.

tests/r1cs.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,92 @@ fn example_gadget_serialization_test() {
358358
// (3 + 4) * (6 + 1) != (40 + 10)
359359
assert!(example_gadget_roundtrip_serialization_helper(3, 4, 6, 1, 40, 10).is_err());
360360
}
361+
362+
// Range Proof gadget
363+
364+
/// Enforces that the quantity of v is in the range [0, 2^n).
365+
pub fn range_proof<CS: ConstraintSystem>(
366+
cs: &mut CS,
367+
mut v: LinearCombination,
368+
v_assignment: Option<u64>,
369+
n: usize,
370+
) -> Result<(), R1CSError> {
371+
let mut exp_2 = Scalar::one();
372+
for i in 0..n {
373+
// Create low-level variables and add them to constraints
374+
let (a, b, o) = cs.allocate_multiplier(v_assignment.map(|q| {
375+
let bit: u64 = (q >> i) & 1;
376+
((1 - bit).into(), bit.into())
377+
}))?;
378+
379+
// Enforce a * b = 0, so one of (a,b) is zero
380+
cs.constrain(o.into());
381+
382+
// Enforce that a = 1 - b, so they both are 1 or 0.
383+
cs.constrain(a + (b - 1u64));
384+
385+
// Add `-b_i*2^i` to the linear combination
386+
// in order to form the following constraint by the end of the loop:
387+
// v = Sum(b_i * 2^i, i = 0..n-1)
388+
v = v - b * exp_2;
389+
390+
exp_2 = exp_2 + exp_2;
391+
}
392+
393+
// Enforce that v = Sum(b_i * 2^i, i = 0..n-1)
394+
cs.constrain(v);
395+
396+
Ok(())
397+
}
398+
399+
#[test]
400+
fn range_proof_gadget() {
401+
use rand::rngs::OsRng;
402+
use rand::Rng;
403+
404+
let mut rng = OsRng::new().unwrap();
405+
let m = 3; // number of values to test per `n`
406+
407+
for n in [2, 10, 32, 63].iter() {
408+
let (min, max) = (0u64, ((1u128 << n) - 1) as u64);
409+
let values: Vec<u64> = (0..m).map(|_| rng.gen_range(min, max)).collect();
410+
for v in values {
411+
assert!(range_proof_helper(v.into(), *n).is_ok());
412+
}
413+
assert!(range_proof_helper((max + 1).into(), *n).is_err());
414+
}
415+
}
416+
417+
fn range_proof_helper(v_val: u64, n: usize) -> Result<(), R1CSError> {
418+
// Common
419+
let pc_gens = PedersenGens::default();
420+
let bp_gens = BulletproofGens::new(128, 1);
421+
422+
// Prover's scope
423+
let (proof, commitment) = {
424+
// Prover makes a `ConstraintSystem` instance representing a range proof gadget
425+
let mut prover_transcript = Transcript::new(b"RangeProofTest");
426+
let mut rng = rand::thread_rng();
427+
428+
let mut prover = Prover::new(&bp_gens, &pc_gens, &mut prover_transcript);
429+
430+
let (com, var) = prover.commit(v_val.into(), Scalar::random(&mut rng));
431+
assert!(range_proof(&mut prover, var.into(), Some(v_val), n).is_ok());
432+
433+
let proof = prover.prove()?;
434+
435+
(proof, com)
436+
};
437+
438+
// Verifier makes a `ConstraintSystem` instance representing a merge gadget
439+
let mut verifier_transcript = Transcript::new(b"RangeProofTest");
440+
let mut verifier = Verifier::new(&bp_gens, &pc_gens, &mut verifier_transcript);
441+
442+
let var = verifier.commit(commitment);
443+
444+
// Verifier adds constraints to the constraint system
445+
assert!(range_proof(&mut verifier, var.into(), None, n).is_ok());
446+
447+
// Verifier verifies proof
448+
Ok(verifier.verify(&proof)?)
449+
}

0 commit comments

Comments
 (0)