-
Notifications
You must be signed in to change notification settings - Fork 523
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 multi-phase circuit #593
base: main
Are you sure you want to change the base?
Conversation
halo2_proofs/src/plonk/prover.rs
Outdated
if self.current_phase > challenge.phase() { | ||
Some(self.challenges[challenge.index()]) | ||
} else { | ||
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.
This is brittle. self.challenges
is initialized to all-zeros, so if there is a bug here or a bug at the point where self.challenges
is mutated that causes a challenge to not be added, then get_challenge
will return zero, which must never happen (as zero-value challenges collapse the things they are designed to separate).
We should instead use a HashMap
here:
if self.current_phase > challenge.phase() { | |
Some(self.challenges[challenge.index()]) | |
} else { | |
None | |
} | |
self.challenges.get(challenge.index()) |
and then only add challenges to self.challenges
as we reach their respective phases. This safely fails-shut, where if there is a bug at the point where self.challenges
is mutated that causes a challenge to not be added, get_challenge
will return None
.
I can see a possible performance rationale for using Vec
over HashMap
, but we should only do that if it actually proves to be necessary, and only after we have this working safely.
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.
Updated in b2626b5
.
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 suggest also adding Expression::Postprocess to ensure we can apply any black-box computations to challenges.
It is partially implemented here: https://github.com/levs57/halo2 , though it probably should be refactored a bit, because right now the fact that Expression:Postprocess is formed only from Expression::Constant, Expression::Challenge or Expression::Postprocess is checked in runtime, while we could achieve the same using wrapper struct PublicExpression with private field Expression.
Usage example here: https://github.com/levs57/halo2-liam-eagen-msm/blob/master/src/testing_stuff/challenge_postprocessing_test.rs
/// Phase of this advice column | ||
phase: Phase, |
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 shouldn't be part of the public API (otherwise someone could mess with the queried expression to break the safety property we are trying to enforce).
We should instead make this entire enum struct opaque (which probably should have been the case for all of Expression
, but it's easy to forget that expression structs and tuples have the visibility of the enum). This is probably best done in a separate PR first.
@@ -493,8 +571,9 @@ impl<F: Field> Expression<F> { | |||
constant: &impl Fn(F) -> T, | |||
selector_column: &impl Fn(Selector) -> T, | |||
fixed_column: &impl Fn(usize, usize, Rotation) -> T, | |||
advice_column: &impl Fn(usize, usize, Rotation) -> T, | |||
advice_column: &impl Fn(usize, usize, Rotation, Phase) -> T, |
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.
These would then take the new AdviceQuery
struct or whatever it gets called.
halo2_proofs/src/plonk/circuit.rs
Outdated
/// Allocate a new challenge in given phase | ||
pub fn challenge_in(&mut self, phase: Phase) -> Challenge { |
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.
Per my earlier comment, the challenge should not be interpreted as allocated "in" phase
, but instead between phase
and phase + 1
. I don't have any great succinct naming suggestions right now, so maybe we should just be verbose:
/// Allocate a new challenge in given phase | |
pub fn challenge_in(&mut self, phase: Phase) -> Challenge { | |
/// Requests a challenge that is usable after the given phase. | |
pub fn challenge_usable_after(&mut self, phase: Phase) -> Challenge { |
This is also where I think using type markers instead of an enum would give a better public API, so the Phase
enum could be entirely private:
/// Allocate a new challenge in given phase | |
pub fn challenge_in(&mut self, phase: Phase) -> Challenge { | |
/// Requests a challenge that is usable after the given phase. | |
pub fn challenge_usable_after<P: Phase>(&mut self, phase: P) -> Challenge { |
and then
trait SealedPhase {
fn to_enum(self) -> PhaseEnum;
}
pub trait Phase: SealedPhase {}
enum FirstPhase {}
impl SealedPhase for FirstPhase {
fn to_enum(self) -> PhaseEnum {
PhaseEnum::First
}
}
impl Phase for FirstPhase {}
Challenge
could still store PhaseEnum
internally to stop the Phase
trait bound from propagating further than this API (which it has no need to, as phases don't need to appear anywhere else).
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.
While I was trying sealed pattern for this, I found that PhaseEnum
might still need to be public because Advice
and Any::Advice
has field of it, and some failure test cases need to create VirtualCell
to be matched, which then needs to specify advice column's phase (even it doesn't use multi-phase feature).
Perhaps we might need to create VirtualCell
in a way that it doesn't need PhaseEnum
?
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.
Yeah, this should be handled by changing the way VirtualCell
works. If the dev tooling is the only thing placing this requirement on the public API, it's the dev tooling we should 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.
I suspect the easiest change here is to add a VirtualColumn
that allows setting the phase for advice, and then have an impl From<Any> for VirtualColumn
etc. We could also keep the existing From
impls, so fixed and instance checks wouldn't have to 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.
I see, it makes sense to me, will try to add VirtualColumn
.
The other issue I have just encountered is in Expression::evaluate
, if we have PhaseEnum
as private enum, what should we pass to the function to evaluate advice column (we can't pass PhaseEnum
because Expression::evaluate
is public)? Or we should first wrap the fields of Expression::{Advice,Fixed,Instance}
as struct {Advice,Fixed,Instance}Query
like you mentioned in #593 (comment), then we can hide the phase information from user.
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.
Yes, I think introducing *Query
structs is the way to go here; then future changes like this will be simpler, the evaluate interface will be simpler, and we can control what is exposed.
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 see, I tried to introduce *Query
structures in #616, and next I will try to rebase on it to continue this pattern.
Ignoring off-phase assignments is the correct behaviour. There is no way to construct a column with an arbitrary phase other than during What we could improve however is error handling. If a user accidentally calls |
We now have this in #598. |
ecd7081
to
31daa6b
Compare
@@ -10,6 +10,18 @@ and this project adheres to Rust's notion of | |||
- `halo2_proofs::circuit::Value`, a more usable and type-safe replacement for | |||
`Option<V>` in circuit synthesis. | |||
- `impl Mul<F: Field> for &Assigned<F>` | |||
- `halo2_proofs::plonk`: |
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.
Now that we've released halo2_proofs 0.2.0
, this PR needs to be rebased (because this section of the changelog is now part of 0.2.0
. Additionally, the commits should be squashed together so that the hard-coded proof binary is unaltered by the branch (rather than having multiple changes across several commits that together are a no-op but cost commit size).
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.
Make sense, I have rebased and squashed all commits into 6 ones with suggestions and fixes above (and I resolved some minor comments, only leave those I'm not sure how to resolve for further discussion), hope it look cleaner now.
@@ -580,7 +580,7 @@ pub mod tests { | |||
offset: 1, | |||
}, | |||
cell_values: vec![( | |||
((Any::Advice, 5).into(), 0).into(), | |||
((Any::Advice { phase: First }, 5).into(), 0).into(), |
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.
These would become something like:
((Any::Advice { phase: First }, 5).into(), 0).into(), | |
((VirtualColumn::Advice(First), 5).into(), 0).into(), |
halo2_proofs/src/plonk/circuit.rs
Outdated
@@ -1357,6 +1573,38 @@ impl<F: Field> ConstraintSystem<F> { | |||
tmp | |||
} | |||
|
|||
/// Requests a challenge that is usable after the given phase. | |||
pub fn challenge_usable_after(&mut self, phase: Phase) -> Challenge { | |||
self.assert_no_phase_skipped(phase, "Challenge"); |
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.
If we're requiring that phases are assigned in order, then we should be checking here that phase
exists, not previous_phase
(since this challenge only makes sense if there are columns in phase
).
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.
Ah I didn't think about that. The assertion helper to check if specified phase exists is refactored in 6051345
.
31daa6b
to
815ce5e
Compare
let [theta, gamma] = [(); 2].map(|_| meta.challenge_usable_after(First)); | ||
// Any phase skipped will cause a panic | ||
assert!(catch_unwind_silent(|| meta.challenge_usable_after(Second)).is_err()); | ||
assert!(catch_unwind_silent(|| meta.advice_column_in(Third)).is_err()); |
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 would be an appropriate thing to check for in a test, but examples should just show the normal usage of the API, not demonstrate error conditions (especially error conditions we've chosen to expose as panics).
This PR aims to support multi-phase circuit (closes #527). The main changes include:
commit@c87ba7a
Phase
, which has 3 variantFirst
,Second
andThird
(3 phases should be enough for most cases in my imagination).phase
to structAdvice
, variantAny::Advice
and variantExpression::Advice
.commit@6051345
Challenge
and can be allocated byConstraintSystem::challenge_usable_after(phase)
, and it's alsoVirtualCells::query_challenge(challenge)
to getExpression::Challenge
.Layouter::get_challenge(challenge)
to getValue<F>
(Value::unknown
if current phase is earlier than requestedchallenge
).create_proof
,ConcreteCircuit::FloorPlanner::synthesize
will be call as many time as the phases used in the circuit, andWitnessCollection
will ignore the advice column assignment if phase doesn't match.Also it adds a new example
shuffle.rs
to show how to use multi-phase to roll our own vector shuffle argument.Todos
Concerns
Handling advice column assignment in wrong phase
(answered in #593 (comment))
Instead of returning an error, currently
WitnessCollection
ignores the advice column assignment if phase doesn't match (like this). The reason is to make sure the regions measured byV1
planner to be same in different phase (To me it's like in key generation (phase 0) we still need to callassign_advice
).One approach is to call and check the value closure returns an
Err(Error::UnmatchedPhase)
, but not sure if this is desired, because it breaks the principle that all value closures passed to anyRegion::assign_*
method will be called at most once.PinnedConstraintSystem
is updated, so asPinnedVerificationKey
(resolved by custom
fmt::Debug
impl)Since a new field is added to
Advice
, and several fieldsnum_challenges
,advice_column_phase
andchallenge_phase
are added toConstraintSystem
, thePinnedConstraintSystem
is also updated to carry these information. So this PR makesVerifyingKey::hash_into
to have different result, and might break some existing prover/verifier that relies on serialized pk/vk (a breaking change tohalo2
).Effect to small circuit that doesn't need multi-phase
The memory usage of
Column<Advice>
is increased to12
or16
bytes, and several fields are addedConstraintSystem
which also increases memory usage. This might affect small circuit's performance.The benchmark or
orchard
circuit seems to be unstable on my mac (Intel Core i5), but here is the result:As-is
halo2_proof@c0db68a
With this PR
halo2_proof@1e96c83