-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support R1CS #52
Support R1CS #52
Conversation
739b5fa
to
207636f
Compare
1fc6da1
to
74944e6
Compare
74944e6
to
239ae52
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really great! I left you a few comments. But the important ones:
- did we forget to constrain the returned CellVars with the public output CellVars o_O? https://zksecurity.github.io/noname/public-outputs.html doesn't even mention it ;_;
- it'd be nice to document the snarkjs expected serialization for .r1cs and .witness, maybe in the book?
- I don't think we should use a hashmap for the symbolic witness vector, I think we can simplify that a lot
var | ||
} | ||
|
||
fn add_constant( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add_constant doesn't make sense in r1cs, I guess we should discuss in #34 how we can remove it
src/backends/r1cs/mod.rs
Outdated
let c = LinearCombination { | ||
terms: Some(HashMap::from_iter(vec![(res, one)])), | ||
constant: None, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LinearCombination::from_var(res)
would be cleaner
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
implemented from_vars :D
} | ||
} | ||
|
||
// todo: how can we create this r1cs backend with different associated field types, but still using the same backend implementation? using macro? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need to use macro, you could just have a R1CS
struct generic on the prime, and then instantiate it in two different backends.
src/backends/r1cs/mod.rs
Outdated
// generate witness through witness vars vector | ||
let mut witness = self.witness_vars.iter().enumerate().map(|(index, val)| { | ||
self.compute_val(witness_env, val, index) | ||
}).collect::<crate::error::Result<Vec<Fr>>>()?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait so, my understanding is that either the public_output values are Value::PublicOutput(None)
, in which case it will crash. Or they are set, in which case they will be calculated correctly at this point.
If the circuit is finalized, they should be set, so we shouldn't have to compute them after no (which is what you do after)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The public output cell is replaced in the finalize_circuit
:
noname/src/backends/r1cs/mod.rs
Lines 215 to 221 in 77e4409
for (pub_var, ret_var) in cvars.clone().iter().zip(returned_cells.unwrap()) { | |
// replace the computation of the public output vars with the actual variables being returned here | |
let var_idx = pub_var.idx().unwrap(); | |
let prev = &self.witness_vars[var_idx]; | |
assert!(matches!(prev, Value::PublicOutput(None))); | |
self.witness_vars[var_idx] = Value::PublicOutput(Some(ret_var)); | |
} |
So actually they can be calculated directly via this line of code, and the following lines from generate_witness
are not necessary any more:
noname/src/backends/r1cs/mod.rs
Lines 266 to 269 in 77e4409
for var in &self.public_outputs { | |
let val = self.compute_var(witness_env, *var)?; | |
witness[var.index] = val; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe my reasoning in the below comment makes sense?
// so we need to compute them separately | ||
for var in &self.public_outputs { | ||
let val = self.compute_var(witness_env, *var)?; | ||
witness[var.index] = val; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so I think this is unnecessary, I guess you can quickly check this by checking if the public_output.no
leads to the same witness with this line or without (or if the witness verifies without this line)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK so thinking more about it, I think I understand why I did this in noname (I really should have documented that).
I think it I compute the public output before everything else, it might end up recursing A LOT as it needs to compute the whole main function to compute the value of the public output. So by deferring we can just make use of the cache to compute the public output and avoid recursing. (I think we should add a comment to explain that in both kimchi and R1CS if we do this ^^)
so in the map above, I think we should just insert 0s if it's a publicoutput, otherwise call compute_val. Then we do that loop here^
wdyt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it might end up recursing A LOT as it needs to compute the whole main function to compute the value of the public output
Because there will be caches created for most of vars when the calculation of the public output triggers the chain of calculations, does that mean the performance gain actually isn't that much for deferring these output calculation until the end of witness generation?
If so, maybe it is simpler to just remove the following code(the redundant calculation), so we can avoid a bit of this intricated assumption?
noname/src/backends/r1cs/mod.rs
Lines 262 to 267 in 6c99128
// The original vars of public outputs are not part of the constraints | |
// so we need to compute them separately | |
for var in &self.public_outputs { | |
let val = self.compute_var(witness_env, *var)?; | |
witness[var.index] = val; | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no I think we should def defer the computation, as the compute_val
will recurse too much otherwise (it would be fine if the implementation was not recursive or didn't use too much memory, but atm it doesn't make sense to not defer IMO).
Because at the point where we call it there is no cache yet (you can probably see that by step debugging with that change)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything looks great, besides the public output computation not being deferred. So I think this is approvable now :) feel free to merge and fix things later!
@@ -21,7 +20,7 @@ where | |||
{ | |||
pub var_values: HashMap<String, Vec<F>>, | |||
|
|||
pub cached_values: HashMap<CellVar, F>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note that this change is a tiny bit annoying because we're exposing usize
to the frontend, which for the frontend doesn't mean much. IMO we should try to keep exposing CellVar
, especially as we might make future changes that might expose a different CellVar
depending on the backend.
I've looked a bit more at the implementation of the compute_val
and I think we could call compute_var
instead (by recreating a CellVar
potentially, or storing a CellVar
in the right place instead of a usize
.
Also I'm not sure I understand why compute_var and compute_val return an Result
:D it looks like it's only for the public output case, but I'm wondering if we can actually reach that path.
Anyway it's not really related to this PR at this point.
src/backends/r1cs/mod.rs
Outdated
} | ||
|
||
// check if every cell vars end up being a cell var in the circuit or public output | ||
for index in 0..self.witness_vars.len() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dangerous! always use iterators (for witness_var in &self.witness_vars
)
9a491f5
to
747c6e2
Compare
Updated to defer the output calculation and optimize the circuit finalization. |
snarkjs testing workflow
Here are the commands to verify the whole process.
Missing meta data
witness data arrangement
witness_vars
)constraint public inputs / outputs
Implement finalize_circuit interface