Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

copy-fix-terminate: enforce secure termination of the copy circuit #1568

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bus-mapping/src/circuit_input_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ impl CircuitInputBuilder<DynamicCParams> {
.iter()
.fold(0, |acc, c| acc + c.bytes.len())
* 2
+ 2;
+ 4; // disabled and unused rows.

let total_rws_before_padding: usize =
<RWCounter as Into<usize>>::into(self.block_ctx.rwc) - 1; // -1 since rwc start from index `1`
Expand Down
9 changes: 9 additions & 0 deletions gadgets/src/binary_number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ where
}
}

/// Return the constant that represents a given value. To be compared with the value expression.
pub fn constant_expr<F: Field>(&self, value: T) -> Expression<F> {
let f = value.as_bits().iter().fold(
F::ZERO,
|result, bit| if *bit { F::ONE } else { F::ZERO } + result * F::from(2),
);
Expression::Constant(f)
}

/// Returns a function that can evaluate to a binary expression, that
/// evaluates to 1 if value is equal to value as bits. The returned
/// expression is of degree N.
Expand Down
81 changes: 57 additions & 24 deletions zkevm-circuits/src/copy_circuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,26 @@ use bus_mapping::{
};
use eth_types::Field;
use gadgets::{
binary_number::BinaryNumberChip,
binary_number::{BinaryNumberChip, BinaryNumberConfig},
less_than::{LtChip, LtConfig, LtInstruction},
util::{and, not, or, Expr},
};
use halo2_proofs::{
circuit::{Layouter, Region, Value},
plonk::{Advice, Column, ConstraintSystem, Error, Expression, Fixed, SecondPhase, Selector},
plonk::{
Advice, Column, ConstraintSystem, Error, Expression, Fixed, SecondPhase, Selector,
VirtualCells,
},
poly::Rotation,
};
use itertools::Itertools;
use std::marker::PhantomData;

// Rows to enable but not use, that can be queried safely by the last event.
const UNUSED_ROWS: usize = 2;
// Rows to disable, so they do not query into Halo2 reserved rows.
const DISABLED_ROWS: usize = 2;

/// The rw table shared between evm circuit and state circuit
#[derive(Clone, Debug)]
pub struct CopyCircuitConfig<F> {
Expand Down Expand Up @@ -161,6 +169,8 @@ impl<F: Field> SubCircuitConfig<F> for CopyCircuitConfig<F> {
]),
);

constrain_must_terminate(&mut cb, meta, q_enable, &tag);

let not_last_two_rows = 1.expr()
- meta.query_advice(is_last, Rotation::cur())
- meta.query_advice(is_last, Rotation::next());
Expand Down Expand Up @@ -468,6 +478,29 @@ impl<F: Field> SubCircuitConfig<F> for CopyCircuitConfig<F> {
}
}

/// Verify that is_last goes to 1 at some point.
pub fn constrain_must_terminate<F: Field>(
cb: &mut BaseConstraintBuilder<F>,
meta: &mut VirtualCells<'_, F>,
q_enable: Column<Fixed>,
tag: &BinaryNumberConfig<CopyDataType, 3>,
) {
// If an event has started (tag != Padding on reader and writer rows), require q_enable=1 at the
// next step. This prevents querying rows where constraints are disabled.
//
// The tag is then copied to the next step by "rows[0].tag == rows[2].tag". Eventually,
// q_enable=0. By that point the tag must have switched to Padding, which is only possible with
// is_last=1. This guarantees that all the final conditions are checked.
let is_event = tag.value(Rotation::cur())(meta) - tag.constant_expr::<F>(CopyDataType::Padding);
Copy link
Collaborator

Choose a reason for hiding this comment

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

tag.constant_expr::<F>(CopyDataType::Padding) part doesn't make sense to me.
Why are we binary decomposing a known value into bits, then binary composing back to constant, instead of using the known value directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed the constant should happen to equal Padding as u64. However, in this context the tag is not a number but a T: AsBits<N>, so this maintain the abstraction by mirroring the value() method just above.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense now.

cb.condition(is_event, |cb| {
cb.require_equal(
"the next step is enabled",
meta.query_fixed(q_enable, Rotation(2)),
1.expr(),
Comment on lines +498 to +499
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a no-op constraint. q_enable is a fixed column determined at setup time, so there is no point constraining a fixed value equal to a constant.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this would make sense if q_enabled could be 0 in some of the non-padding rows, but this indeed probably shouldn't happen at all as q_enabled is a fixed column and thus determined in advance as CC pointed out - q_enabled just needs to be 1 everywhere I think (at least in some older versions of Halo2, the lookups required to have a fixed column I think, so we need to have at least one fixed column).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It reads like this: if we are processing an event, then we are not about to hit the end of the circuit.

At the end of the circuit, q_enable switches to 0, which is how we detect the end. The rows just before that end are forced to be padding (is_event=0). This forces the last event to be fully verified by then.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, if the number of rows with q_enabled = true is constant (which it is), then I think this is all good.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Hi @naure, what you say makes sense to me now, but I have a nitpick for readability.
It looks like if we unroll this constraint, it will be

is_event * (q_enable - 1)

Which is the same as (q_enable - 1) * is_event. The latter way fits the Selector/Fixed column first convention.

Can we rewrite it to something like this for readability?

 cb.condition(meta.query_fixed(q_enable, Rotation(2)) - 1, |cb| {
        cb.require_equal(
            "The final step must be padding",
            is_event,
            0.expr(),

);
});
}

impl<F: Field> CopyCircuitConfig<F> {
/// Assign an individual copy event to the Copy Circuit.
pub fn assign_copy_event(
Expand Down Expand Up @@ -566,11 +599,11 @@ impl<F: Field> CopyCircuitConfig<F> {
) -> Result<(), Error> {
let copy_rows_needed = copy_events.iter().map(|c| c.bytes.len() * 2).sum::<usize>();

// The `+ 2` is used to take into account the two extra empty copy rows needed
// to satisfy the query at `Rotation(2)` performed inside of the
// `rows[2].value == rows[0].value * r + rows[1].value` requirement in the RLC
// Accumulation gate.
assert!(copy_rows_needed + 2 <= max_copy_rows);
assert!(
copy_rows_needed + DISABLED_ROWS + UNUSED_ROWS <= max_copy_rows,
"copy rows not enough {copy_rows_needed} + 4 vs {max_copy_rows}"
);
let filler_rows = max_copy_rows - copy_rows_needed - DISABLED_ROWS;

let tag_chip = BinaryNumberChip::construct(self.copy_table.tag);
let lt_chip = LtChip::construct(self.addr_lt_addr_end);
Expand All @@ -597,12 +630,14 @@ impl<F: Field> CopyCircuitConfig<F> {
)?;
}

for _ in 0..max_copy_rows - copy_rows_needed - 2 {
self.assign_padding_row(&mut region, &mut offset, false, &tag_chip, &lt_chip)?;
for _ in 0..filler_rows {
self.assign_padding_row(&mut region, &mut offset, true, &tag_chip, &lt_chip)?;
ChihChengLiang marked this conversation as resolved.
Show resolved Hide resolved
}
assert_eq!(offset % 2, 0, "enabled rows must come in pairs");

self.assign_padding_row(&mut region, &mut offset, true, &tag_chip, &lt_chip)?;
self.assign_padding_row(&mut region, &mut offset, true, &tag_chip, &lt_chip)?;
for _ in 0..DISABLED_ROWS {
self.assign_padding_row(&mut region, &mut offset, false, &tag_chip, &lt_chip)?;
}

Ok(())
},
Expand All @@ -613,22 +648,20 @@ impl<F: Field> CopyCircuitConfig<F> {
&self,
region: &mut Region<F>,
offset: &mut usize,
is_last_two: bool,
enabled: bool,
tag_chip: &BinaryNumberChip<F, CopyDataType, 3>,
lt_chip: &LtChip<F, 8>,
) -> Result<(), Error> {
if !is_last_two {
// q_enable
region.assign_fixed(
|| "q_enable",
self.q_enable,
*offset,
|| Value::known(F::ONE),
)?;
// q_step
if *offset % 2 == 0 {
self.q_step.enable(region, *offset)?;
}
// q_enable
region.assign_fixed(
|| "q_enable",
self.q_enable,
*offset,
|| Value::known(if enabled { F::ONE } else { F::ZERO }),
)?;
// q_step
if enabled && *offset % 2 == 0 {
self.q_step.enable(region, *offset)?;
}

// is_first
Expand Down