Skip to content
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

PI handling & Circuit Builder trait refactor #423

Merged
merged 24 commits into from Mar 4, 2021

Conversation

CPerezz
Copy link
Contributor

@CPerezz CPerezz commented Mar 3, 2021

As it was mentioned in the latest iteration, for plonk v0.6 we wanted to improve the API that the Circuit trait was exposing and forcing users to implement as well as find better ways of handling the public inputs.

Finally with this PR, we've been able to close some issues related to the problems mentioned above. Here is a summary of the problems that the PR addresses:

@CPerezz CPerezz added the team:Core Low Level Core Development Team (Rust) label Mar 3, 2021
@CPerezz CPerezz changed the title [WIP] PI handling & Circuit Builder trait refactor PI handling & Circuit Builder trait refactor Mar 3, 2021
@CPerezz CPerezz marked this pull request as ready for review March 3, 2021 18:18
@CPerezz CPerezz requested review from vlopes11, ZER0 and LukePearson1 and removed request for vlopes11 March 3, 2021 18:51
@CPerezz CPerezz changed the base branch from master to release-0.6 March 3, 2021 19:29
Copy link
Contributor

@vlopes11 vlopes11 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few nits

@@ -74,7 +74,9 @@ impl StandardComposer {
self.q_fixed_group_add.push(BlsScalar::zero());
self.q_variable_group_add.push(BlsScalar::zero());

self.public_inputs.push(pi);
if let Some(pi) = pi {
assert!(self.public_inputs_sparse_store.insert(self.n, pi).is_none());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we should go in a direction to reduce the number of assertions inside plonk functions, not increase it. If the PI must fail in this case, then the function should return a Result

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me the reason why we have such assertion, so I cannot judge it properly.
So, first of all I would add a comment about why this is needed.
Basically it should clarify:

  1. Why assert! is needed?
  2. Why assert! instead of debug_assert!?

If we're going to keep the it, we also should add a message so if it fails we know what failed and why.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never happen. That's why it has an assert! it's like unreachable!() for me here. I just have it so that if at some point this assertion fails. We know that we made a change in a really wrong direction.

@@ -153,7 +155,9 @@ impl StandardComposer {
self.q_fixed_group_add.push(BlsScalar::zero());
self.q_variable_group_add.push(BlsScalar::zero());

self.public_inputs.push(pi);
if let Some(pi) = pi {
assert!(self.public_inputs_sparse_store.insert(self.n, pi).is_none());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary assertion, check above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as mentioned before.

self.public_inputs_sparse_store
.keys()
.map(|pos| *pos)
.collect::<Vec<usize>>()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary type annotation

@@ -206,7 +229,9 @@ impl StandardComposer {
self.q_fixed_group_add.push(BlsScalar::zero());
self.q_variable_group_add.push(BlsScalar::zero());

self.public_inputs.push(pi);
if let Some(pi) = pi {
assert!(self.public_inputs_sparse_store.insert(self.n, pi).is_none());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary assertion, check above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment I made in the previous one.

#[cfg_attr(feature = "canon", derive(Canon))]
/// Structure that represents a PLONK Circuit Public Input
/// structure converted into it's &[BlsScalar] repr.
pub struct PublicInputValue(pub(crate) Vec<BlsScalar>);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name suggests its a single public input when its in fact a collection

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is correct: A single public input value is defined by one or more BlsScalar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And in fact it is a Public Input. But expressed on it's BlsScalar form. And as you know. Since we have 3 types that are Public Inputs and one of them (AffinePoint) gets represented as 2 BlsScalar instead of one.

Therefore the best way we've found with @ZER0 for now is to return a Vec<BlsScalar> which will be 1 item for BlsScalar and JubJubScalar and 2 for AffinePoint.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before it was an enum. Do we have a gain to convert from enum to this Vec structure? Also, if we know the size to be either 1 or 2, any of these simpler structures will also do:

  • (BlsScalar, Option<BlsScalar>)
  • [Option<BlsScalar>; 2]
  • [BlsScalar; 2]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But Vec seemed much simpler than that. The only one that I might adopt is the Array. But is more missleading since makes it look like it's always 2 arrays instead of one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As alternative we could use const generic also here, you can play with this if you want and see how the API looks like.

bytes
}
}
impl From<BlsScalar> for PublicInputValue {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should we reconstruct a collection of public inputs from a single scalar?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a collection of public input, is a single public input. Maybe we could add a more descriptive documentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its confusing to have a Vec and not expect a variable size collection

_ => unreachable!(),
}
}
impl From<JubJubScalar> for PublicInputValue {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single scalar should not result in a collection; check above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case yes. Basically a PublicInputValue in our case can be one or more (currently two) BlsScalar. But it doesn't matter the underling representation, the entity is still a single PublicInputValue.

PublicInput::JubJubScalar(_, pos) => [*pos, 0],
PublicInput::AffinePoint(_, pos_x, pos_y) => [*pos_x, *pos_y],
}
impl From<JubJubAffine> for PublicInputValue {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single point should not result in a collection, check above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, yes. A single point can be decoded as two BlsScalar (x and y in this case).

/// Initialization string used to fill the transcript for both parties.
const TRANSCRIPT_INIT: &'static [u8];
/// Trimming size for the keys of the circuit.
const TRIM_SIZE: usize;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TRIM_SIZE can't be constant for a circuit. One example is the transfer circuit that has variable circuit size depending on the number of inputs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked the same question today, if it could be a constant or not. Maybe @CPerezz was misleaded by #351 ? I don't know. Maybe tomorrow morning at the stand up we should discuss this point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it when I was implementing it.
What if we pass it with a const generic? SO that the trim size also constrains the type @ZER0 @vlopes11 ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's talk about it tomorrow morning. It's crucial to fully understand the edge case of transfer circuit, it's the only anomaly there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#351 is only about the set_trim_size that should never be called, as in the proposal inside the issue

Proposal: remove Circuit::set_trim_size

However, the get_trim_size (or any other method name with the same effect) will return the current size for that circuit. There is the "con" to tie the circuit instance to a fixed size trim without any clear "pro"

pub_input_pos: &PublicInputPositions,
) -> Vec<BlsScalar> {
let mut pi = vec![BlsScalar::zero(); Self::TRIM_SIZE];
println!("{:?}", pub_input_pos);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove println

@CPerezz CPerezz requested a review from vlopes11 March 3, 2021 22:03
Copy link
Contributor

@ZER0 ZER0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few things to fix, but overall is good.

@@ -74,7 +74,9 @@ impl StandardComposer {
self.q_fixed_group_add.push(BlsScalar::zero());
self.q_variable_group_add.push(BlsScalar::zero());

self.public_inputs.push(pi);
if let Some(pi) = pi {
assert!(self.public_inputs_sparse_store.insert(self.n, pi).is_none());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unclear to me the reason why we have such assertion, so I cannot judge it properly.
So, first of all I would add a comment about why this is needed.
Basically it should clarify:

  1. Why assert! is needed?
  2. Why assert! instead of debug_assert!?

If we're going to keep the it, we also should add a message so if it fails we know what failed and why.

@@ -153,7 +155,9 @@ impl StandardComposer {
self.q_fixed_group_add.push(BlsScalar::zero());
self.q_variable_group_add.push(BlsScalar::zero());

self.public_inputs.push(pi);
if let Some(pi) = pi {
assert!(self.public_inputs_sparse_store.insert(self.n, pi).is_none());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as mentioned before.

#[cfg_attr(feature = "canon", derive(Canon))]
/// Structure that represents a PLONK Circuit Public Input
/// structure converted into it's &[BlsScalar] repr.
pub struct PublicInputValue(pub(crate) Vec<BlsScalar>);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is correct: A single public input value is defined by one or more BlsScalar.

bytes
}
}
impl From<BlsScalar> for PublicInputValue {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a collection of public input, is a single public input. Maybe we could add a more descriptive documentation.

_ => unreachable!(),
}
}
impl From<JubJubScalar> for PublicInputValue {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case yes. Basically a PublicInputValue in our case can be one or more (currently two) BlsScalar. But it doesn't matter the underling representation, the entity is still a single PublicInputValue.

Comment on lines 106 to 107
.keys()
.map(|pos| *pos)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.keys()
.map(|pos| *pos)
.keys()
.copied()

+ (q_r * b_eval)
+ (q_4 * d_eval)
+ q_c
+ pi.unwrap_or(BlsScalar::zero());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
+ pi.unwrap_or(BlsScalar::zero());
+ pi.unwrap_or_default();

or:

Suggested change
+ pi.unwrap_or(BlsScalar::zero());
+ pi.unwrap_or_else(BlsScalar::zero);

@@ -268,7 +276,8 @@ impl StandardComposer {
let a_eval = self.variables[&a];
let b_eval = self.variables[&b];
let d_eval = self.variables[&d];
let c_eval = (q_m * a_eval * b_eval) + (q_4 * d_eval) + q_c + pi;
let c_eval =
(q_m * a_eval * b_eval) + (q_4 * d_eval) + q_c + pi.unwrap_or(BlsScalar::zero());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the comment above.

Comment on lines 93 to 97
self.public_inputs_sparse_store
.keys()
.zip(self.public_inputs_sparse_store.values())
.for_each(|(pos, value)| {
pi[*pos] = *value;
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're zipping yourself. :) It make sense when you have the keys and the values on two different objects, but here the object is just one. You should be able just to iterate it to get the entries (a tuple (key, value)).

@@ -206,7 +229,9 @@ impl StandardComposer {
self.q_fixed_group_add.push(BlsScalar::zero());
self.q_variable_group_add.push(BlsScalar::zero());

self.public_inputs.push(pi);
if let Some(pi) = pi {
assert!(self.public_inputs_sparse_store.insert(self.n, pi).is_none());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment I made in the previous one.

@CPerezz CPerezz requested a review from ZER0 March 4, 2021 08:49
Copy link
Contributor

@vlopes11 vlopes11 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM after the nits pointed by @ZER0 are addressed.

Looking forward to this merge; will help a lot the circuits development

CPerezz added 15 commits March 4, 2021 16:46
It wasted a lot of time and also was useless to call the
`gadget` fn again for the verify fn in the Circuit trait.

Thanks to the refactor done to the PublicInputs and how they're
handled now we no longer need to call it.
Previously we were storing the dense representation
of the public inputs that were used with the `Composer`
instance.

This was constly, since most of the public inputs were zero and
therefore storing them was useless but also we needed to collect
the position information in the Circuit trait in such difficult ways.

With this refactor we currently have:
- Public Inputs are now passed to gate-functions as `Option<BlsScalar>`.
- The `Composer` no longer stores the dense public input vector. Instead
we store two sparse vectors which contain the values and the positions.
- We include a function that allows the `Composer` to construct the
dense PI vector from it's sparse ones.
- We've added ways for the consumer to get access to the positions
vector. This will allow to simplify the public input management on the
`Circuit` trait.
After discussions with @ZER0 we realized there's not a need
to have a really big trait to handle the public inputs and all
we need is a simple structure that implements the conversions from the
different PI types that we can have in PLONK circuits into the format
that PLONK requires which is `&[BlsScalar]`.

- Created `PublicInputValue` which is implemented for `BlsScalar`,
`JubJubScalar` and `JubJubAffine`.
- Forced the `Circuit` trait to use this struct as source of the public
input values needed in the verification step.
Instead of storing the sparse representation of the non-zero
PublicInputs as two different `Vec` which are logically connected but
not tecnollogically(code-wise).

With this change we can have them strictly correlated since they're
stored linked in the `HashMap`.
We need to return the PI positons(and store them) in order since
otherways when consumers ask for these data, it would be returned
wrongly if we don't return it ordered.
This also allows to test wether the support for the PI handling of the
Circuit trait is correct.
To allow (although it's not recommended to do) variable-size circuits,
we need a way to express different circuit sizes (TRIM_SIZE) for the
same circuit.

By setting the `Circuit::TRIM_SIZE` inherit the value from the const
generic parameter N, we enable this behaviour leaving an API that is not
bad.
CPerezz added 9 commits March 4, 2021 16:46
Since these two functions didn't need `Self` or `self` and also
were independent, we've been able to remove them from the trait.

This closes #396 and also allows us to have a generic method that
can verify Proofs of any `Circuit` without needing to have access to the
type of it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
team:Core Low Level Core Development Team (Rust)
Projects
None yet
3 participants