pub(crate) mod util;
#[cfg(any(test, feature = "test-circuits"))]
mod dev;
#[cfg(test)]
mod test;
#[cfg(feature = "test-circuits")]
pub use dev::CopyCircuit as TestCopyCircuit;
use crate::{
evm_circuit::util::constraint_builder::{BaseConstraintBuilder, ConstrainBuilderCommon},
table::{
BytecodeFieldTag, BytecodeTable, CopyTable, LookupTable, RwTable, TxContextFieldTag,
TxTable,
},
util::{Challenges, SubCircuit, SubCircuitConfig},
witness,
witness::{Chunk, Rw, RwMap, Transaction},
};
use bus_mapping::{
circuit_input_builder::{CopyDataType, CopyEvent},
operation::Target,
state_db::CodeDB,
};
use eth_types::Field;
use gadgets::{
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,
VirtualCells,
},
poly::Rotation,
};
use itertools::Itertools;
use std::marker::PhantomData;
const UNUSED_ROWS: usize = 2;
const DISABLED_ROWS: usize = 2;
#[derive(Clone, Debug)]
pub struct CopyCircuitConfig<F> {
pub q_step: Selector,
pub is_last: Column<Advice>,
pub value: Column<Advice>,
pub value_acc_rlc: Column<Advice>,
pub is_pad: Column<Advice>,
pub is_code: Column<Advice>,
pub q_enable: Column<Fixed>,
pub copy_table: CopyTable,
copy_table_tag: BinaryNumberConfig<CopyDataType, 3>,
pub addr_lt_addr_end: LtConfig<F, 8>,
pub tx_table: TxTable,
pub rw_table: RwTable,
pub bytecode_table: BytecodeTable,
}
pub struct CopyCircuitConfigArgs<F: Field> {
pub tx_table: TxTable,
pub rw_table: RwTable,
pub bytecode_table: BytecodeTable,
pub copy_table: CopyTable,
pub q_enable: Column<Fixed>,
pub challenges: Challenges<Expression<F>>,
}
impl<F: Field> SubCircuitConfig<F> for CopyCircuitConfig<F> {
type ConfigArgs = CopyCircuitConfigArgs<F>;
fn new(
meta: &mut ConstraintSystem<F>,
Self::ConfigArgs {
tx_table,
rw_table,
bytecode_table,
copy_table,
q_enable,
challenges,
}: Self::ConfigArgs,
) -> Self {
let q_step = meta.complex_selector();
let is_last = meta.advice_column();
let value = meta.advice_column();
let value_acc_rlc = meta.advice_column_in(SecondPhase);
let is_code = meta.advice_column();
let is_pad = meta.advice_column();
let is_first = copy_table.is_first;
let id = copy_table.id;
let addr = copy_table.addr;
let src_addr_end = copy_table.src_addr_end;
let bytes_left = copy_table.bytes_left;
let rlc_acc = copy_table.rlc_acc;
let rw_counter = copy_table.rw_counter;
let rwc_inc_left = copy_table.rwc_inc_left;
let tag_bits = copy_table.tag;
tx_table.annotate_columns(meta);
rw_table.annotate_columns(meta);
bytecode_table.annotate_columns(meta);
copy_table.annotate_columns(meta);
let tag = BinaryNumberChip::configure(meta, tag_bits, q_enable, None);
let addr_lt_addr_end = LtChip::configure(
meta,
|meta| meta.query_selector(q_step),
|meta| meta.query_advice(addr, Rotation::cur()),
|meta| meta.query_advice(src_addr_end, Rotation::cur()),
);
meta.create_gate("verify row", |meta| {
let mut cb = BaseConstraintBuilder::default();
cb.require_boolean(
"is_first is boolean",
meta.query_advice(is_first, Rotation::cur()),
);
cb.require_boolean(
"is_last is boolean",
meta.query_advice(is_last, Rotation::cur()),
);
cb.require_zero(
"is_first == 0 when q_step == 0",
and::expr([
not::expr(meta.query_selector(q_step)),
meta.query_advice(is_first, Rotation::cur()),
]),
);
cb.require_zero(
"is_last == 0 when q_step == 1",
and::expr([
meta.query_advice(is_last, Rotation::cur()),
meta.query_selector(q_step),
]),
);
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());
cb.condition(
not_last_two_rows
* (not::expr(tag.value_equals(CopyDataType::Padding, Rotation::cur())(
meta,
))),
|cb| {
cb.require_equal_word(
"rows[0].id == rows[2].id",
id.map(|limb| meta.query_advice(limb, Rotation::cur())),
id.map(|limb| meta.query_advice(limb, Rotation(2))),
);
cb.require_equal(
"rows[0].tag == rows[2].tag",
tag.value(Rotation::cur())(meta),
tag.value(Rotation(2))(meta),
);
cb.require_equal(
"rows[0].addr + 1 == rows[2].addr",
meta.query_advice(addr, Rotation::cur()) + 1.expr(),
meta.query_advice(addr, Rotation(2)),
);
cb.require_equal(
"rows[0].src_addr_end == rows[2].src_addr_end for non-last step",
meta.query_advice(src_addr_end, Rotation::cur()),
meta.query_advice(src_addr_end, Rotation(2)),
);
},
);
let rw_diff = and::expr([
or::expr([
tag.value_equals(CopyDataType::Memory, Rotation::cur())(meta),
tag.value_equals(CopyDataType::TxLog, Rotation::cur())(meta),
]),
not::expr(meta.query_advice(is_pad, Rotation::cur())),
]);
cb.condition(
not::expr(meta.query_advice(is_last, Rotation::cur())),
|cb| {
cb.require_equal(
"rows[0].rw_counter + rw_diff == rows[1].rw_counter",
meta.query_advice(rw_counter, Rotation::cur()) + rw_diff.clone(),
meta.query_advice(rw_counter, Rotation::next()),
);
cb.require_equal(
"rows[0].rwc_inc_left - rw_diff == rows[1].rwc_inc_left",
meta.query_advice(rwc_inc_left, Rotation::cur()) - rw_diff.clone(),
meta.query_advice(rwc_inc_left, Rotation::next()),
);
cb.require_equal(
"rows[0].rlc_acc == rows[1].rlc_acc",
meta.query_advice(rlc_acc, Rotation::cur()),
meta.query_advice(rlc_acc, Rotation::next()),
);
},
);
cb.condition(meta.query_advice(is_last, Rotation::cur()), |cb| {
cb.require_equal(
"rwc_inc_left == rw_diff for last row in the copy slot",
meta.query_advice(rwc_inc_left, Rotation::cur()),
rw_diff,
);
});
cb.gate(meta.query_fixed(q_enable, Rotation::cur()))
});
meta.create_gate(
"Last Step (check value accumulator) Memory => Bytecode or RlcAcc",
|meta: &mut halo2_proofs::plonk::VirtualCells<F>| {
let mut cb = BaseConstraintBuilder::default();
cb.require_equal(
"value_acc_rlc == rlc_acc on the last row",
meta.query_advice(value_acc_rlc, Rotation::next()),
meta.query_advice(rlc_acc, Rotation::next()),
);
cb.gate(and::expr([
meta.query_fixed(q_enable, Rotation::cur()),
meta.query_advice(is_last, Rotation::next()),
and::expr([
tag.value_equals(CopyDataType::Memory, Rotation::cur())(meta),
tag.value_equals(CopyDataType::Bytecode, Rotation::next())(meta),
]) + tag.value_equals(CopyDataType::RlcAcc, Rotation::next())(meta),
]))
},
);
meta.create_gate("verify step (q_step == 1)", |meta| {
let mut cb = BaseConstraintBuilder::default();
cb.require_zero(
"bytes_left == 1 for last step",
and::expr([
meta.query_advice(is_last, Rotation::next()),
1.expr() - meta.query_advice(bytes_left, Rotation::cur()),
]),
);
cb.condition(
not::expr(or::expr([
meta.query_advice(is_last, Rotation::next()),
tag.value_equals(CopyDataType::Padding, Rotation::cur())(meta),
])),
|cb| {
cb.require_equal(
"bytes_left == bytes_left_next + 1 for non-last step",
meta.query_advice(bytes_left, Rotation::cur()),
meta.query_advice(bytes_left, Rotation(2)) + 1.expr(),
);
},
);
cb.condition(meta.query_advice(is_first, Rotation::cur()), |cb| {
cb.require_equal(
"value == value_acc_rlc at every first copy event",
meta.query_advice(value, Rotation::cur()),
meta.query_advice(value_acc_rlc, Rotation::cur()),
);
});
cb.require_equal(
"write value == read value",
meta.query_advice(value, Rotation::cur()),
meta.query_advice(value, Rotation::next()),
);
cb.require_equal(
"value_acc_rlc is same for read-write rows",
meta.query_advice(value_acc_rlc, Rotation::cur()),
meta.query_advice(value_acc_rlc, Rotation::next()),
);
cb.condition(
and::expr([
not::expr(meta.query_advice(is_last, Rotation::next())),
not::expr(meta.query_advice(is_pad, Rotation::cur())),
]),
|cb| {
cb.require_equal(
"value_acc_rlc(2) == value_acc_rlc(0) * r + value(2)",
meta.query_advice(value_acc_rlc, Rotation(2)),
meta.query_advice(value_acc_rlc, Rotation::cur())
* challenges.keccak_input()
+ meta.query_advice(value, Rotation(2)),
);
},
);
cb.require_zero(
"value == 0 when is_pad == 1 for read",
and::expr([
meta.query_advice(is_pad, Rotation::cur()),
meta.query_advice(value, Rotation::cur()),
]),
);
cb.require_equal(
"is_pad == 1 - (src_addr < src_addr_end) for read row",
1.expr() - addr_lt_addr_end.is_lt(meta, None),
meta.query_advice(is_pad, Rotation::cur()),
);
cb.require_zero(
"is_pad == 0 for write row",
meta.query_advice(is_pad, Rotation::next()),
);
cb.gate(and::expr([meta.query_selector(q_step)]))
});
meta.lookup_any("Memory lookup", |meta| {
let cond = meta.query_fixed(q_enable, Rotation::cur())
* tag.value_equals(CopyDataType::Memory, Rotation::cur())(meta)
* not::expr(meta.query_advice(is_pad, Rotation::cur()));
vec![
meta.query_advice(rw_counter, Rotation::cur()),
not::expr(meta.query_selector(q_step)),
Target::Memory.expr(),
meta.query_advice(id.lo(), Rotation::cur()), meta.query_advice(addr, Rotation::cur()), 0.expr(), 0.expr(), 0.expr(), meta.query_advice(value, Rotation::cur()), 0.expr(), 0.expr(), 0.expr(), 0.expr(), 0.expr(), ]
.into_iter()
.zip_eq(rw_table.table_exprs(meta))
.map(|(arg, table)| (cond.clone() * arg, table))
.collect()
});
meta.lookup_any("TxLog lookup", |meta| {
let cond = meta.query_fixed(q_enable, Rotation::cur())
* tag.value_equals(CopyDataType::TxLog, Rotation::cur())(meta);
vec![
meta.query_advice(rw_counter, Rotation::cur()),
1.expr(),
Target::TxLog.expr(),
meta.query_advice(id.lo(), Rotation::cur()), meta.query_advice(addr, Rotation::cur()), 0.expr(), 0.expr(), 0.expr(), meta.query_advice(value, Rotation::cur()), 0.expr(), 0.expr(), 0.expr(), 0.expr(), 0.expr(), ]
.into_iter()
.zip_eq(rw_table.table_exprs(meta))
.map(|(arg, table)| (cond.clone() * arg, table))
.collect()
});
meta.lookup_any("Bytecode lookup", |meta| {
let cond = meta.query_fixed(q_enable, Rotation::cur())
* tag.value_equals(CopyDataType::Bytecode, Rotation::cur())(meta)
* not::expr(meta.query_advice(is_pad, Rotation::cur()));
vec![
meta.query_advice(id.lo(), Rotation::cur()),
meta.query_advice(id.hi(), Rotation::cur()),
BytecodeFieldTag::Byte.expr(),
meta.query_advice(addr, Rotation::cur()),
meta.query_advice(is_code, Rotation::cur()),
meta.query_advice(value, Rotation::cur()),
]
.into_iter()
.zip_eq(bytecode_table.table_exprs(meta))
.map(|(arg, table)| (cond.clone() * arg, table))
.collect()
});
meta.lookup_any("Tx calldata lookup", |meta| {
let cond = meta.query_fixed(q_enable, Rotation::cur())
* tag.value_equals(CopyDataType::TxCalldata, Rotation::cur())(meta)
* not::expr(meta.query_advice(is_pad, Rotation::cur()));
vec![
meta.query_advice(id.lo(), Rotation::cur()), TxContextFieldTag::CallData.expr(),
meta.query_advice(addr, Rotation::cur()),
meta.query_advice(value, Rotation::cur()),
]
.into_iter()
.zip(tx_table.table_exprs(meta))
.map(|(arg, table)| (cond.clone() * arg, table))
.collect()
});
meta.create_gate("id_hi === 0 when Memory", |meta| {
let mut cb = BaseConstraintBuilder::default();
let cond = tag.value_equals(CopyDataType::Memory, Rotation::cur())(meta)
* not::expr(meta.query_advice(is_pad, Rotation::cur()));
cb.condition(cond, |cb| {
cb.require_zero("id_hi === 0", meta.query_advice(id.hi(), Rotation::cur()))
});
cb.gate(meta.query_fixed(q_enable, Rotation::cur()))
});
meta.create_gate("id_hi === 0 when TxLog", |meta| {
let mut cb = BaseConstraintBuilder::default();
let cond = tag.value_equals(CopyDataType::TxLog, Rotation::cur())(meta)
* not::expr(meta.query_advice(is_pad, Rotation::cur()));
cb.condition(cond, |cb| {
cb.require_zero("id_hi === 0", meta.query_advice(id.hi(), Rotation::cur()))
});
cb.gate(meta.query_fixed(q_enable, Rotation::cur()))
});
meta.create_gate("id_hi === 0 when TxCalldata", |meta| {
let mut cb = BaseConstraintBuilder::default();
let cond = tag.value_equals(CopyDataType::TxCalldata, Rotation::cur())(meta)
* not::expr(meta.query_advice(is_pad, Rotation::cur()));
cb.condition(cond, |cb| {
cb.require_zero("id_hi === 0", meta.query_advice(id.hi(), Rotation::cur()))
});
cb.gate(meta.query_fixed(q_enable, Rotation::cur()))
});
Self {
q_step,
is_last,
value,
value_acc_rlc,
is_pad,
is_code,
q_enable,
addr_lt_addr_end,
copy_table,
copy_table_tag: tag,
tx_table,
rw_table,
bytecode_table,
}
}
}
pub fn constrain_must_terminate<F: Field>(
cb: &mut BaseConstraintBuilder<F>,
meta: &mut VirtualCells<'_, F>,
q_enable: Column<Fixed>,
tag: &BinaryNumberConfig<CopyDataType, 3>,
) {
let is_event = tag.value(Rotation::cur())(meta) - tag.constant_expr::<F>(CopyDataType::Padding);
cb.condition(is_event, |cb| {
cb.require_equal(
"the next step is enabled",
meta.query_fixed(q_enable, Rotation(2)),
1.expr(),
);
});
}
impl<F: Field> CopyCircuitConfig<F> {
pub fn assign_copy_event(
&self,
region: &mut Region<F>,
offset: &mut usize,
tag_chip: &BinaryNumberChip<F, CopyDataType, 3>,
lt_chip: &LtChip<F, 8>,
challenges: Challenges<Value<F>>,
copy_event: &CopyEvent,
) -> Result<(), Error> {
for (step_idx, (tag, table_row, circuit_row)) in
CopyTable::assignments(copy_event, challenges)
.iter()
.enumerate()
{
let is_read = step_idx % 2 == 0;
for (&column, &(value, label)) in
<CopyTable as LookupTable<F>>::advice_columns(&self.copy_table)
.iter()
.zip_eq(table_row)
{
if (!is_read && (label == "src_addr_end" || label == "bytes_left"))
|| label == "tag_bit"
{
} else {
region.assign_advice(
|| format!("{} at row: {}", label, offset),
column,
*offset,
|| value,
)?;
}
}
if is_read {
self.q_step.enable(region, *offset)?;
}
region.assign_fixed(
|| "q_enable",
self.q_enable,
*offset,
|| Value::known(F::ONE),
)?;
for (column, &(value, label)) in [
self.is_last,
self.value,
self.value_acc_rlc,
self.is_pad,
self.is_code,
]
.iter()
.zip_eq(circuit_row)
{
region.assign_advice(
|| format!("{} at row: {}", label, *offset),
*column,
*offset,
|| value,
)?;
}
tag_chip.assign(region, *offset, tag)?;
if is_read {
lt_chip.assign(
region,
*offset,
Value::known(F::from(
copy_event.src_addr + u64::try_from(step_idx).unwrap() / 2u64,
)),
Value::known(F::from(copy_event.src_addr_end)),
)?;
}
*offset += 1;
}
Ok(())
}
pub fn assign_copy_events(
&self,
layouter: &mut impl Layouter<F>,
copy_events: &[CopyEvent],
max_copy_rows: usize,
challenges: Challenges<Value<F>>,
) -> Result<(), Error> {
let copy_rows_needed = copy_events.iter().map(|c| c.bytes.len() * 2).sum::<usize>();
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);
lt_chip.load(layouter)?;
layouter.assign_region(
|| "assign copy table",
|mut region| {
region.name_column(|| "is_last", self.is_last);
region.name_column(|| "value", self.value);
region.name_column(|| "is_code", self.is_code);
region.name_column(|| "is_pad", self.is_pad);
let mut offset = 0;
for copy_event in copy_events.iter() {
self.assign_copy_event(
&mut region,
&mut offset,
&tag_chip,
<_chip,
challenges,
copy_event,
)?;
}
for _ in 0..filler_rows {
self.assign_padding_row(&mut region, &mut offset, true, &tag_chip, <_chip)?;
}
assert_eq!(offset % 2, 0, "enabled rows must come in pairs");
for _ in 0..DISABLED_ROWS {
self.assign_padding_row(&mut region, &mut offset, false, &tag_chip, <_chip)?;
}
Ok(())
},
)
}
fn assign_padding_row(
&self,
region: &mut Region<F>,
offset: &mut usize,
enabled: bool,
tag_chip: &BinaryNumberChip<F, CopyDataType, 3>,
lt_chip: &LtChip<F, 8>,
) -> Result<(), Error> {
region.assign_fixed(
|| "q_enable",
self.q_enable,
*offset,
|| Value::known(if enabled { F::ONE } else { F::ZERO }),
)?;
if enabled && *offset % 2 == 0 {
self.q_step.enable(region, *offset)?;
}
region.assign_advice(
|| format!("assign is_first {}", *offset),
self.copy_table.is_first,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign is_last {}", *offset),
self.is_last,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign id lo {}", *offset),
self.copy_table.id.lo(),
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign id hi {}", *offset),
self.copy_table.id.hi(),
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign addr {}", *offset),
self.copy_table.addr,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign src_addr_end {}", *offset),
self.copy_table.src_addr_end,
*offset,
|| Value::known(F::ONE),
)?;
region.assign_advice(
|| format!("assign bytes_left {}", *offset),
self.copy_table.bytes_left,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign value {}", *offset),
self.value,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign value_acc_rlc {}", *offset),
self.value_acc_rlc,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign rlc_acc {}", *offset),
self.copy_table.rlc_acc,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign is_code {}", *offset),
self.is_code,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign is_pad {}", *offset),
self.is_pad,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign rw_counter {}", *offset),
self.copy_table.rw_counter,
*offset,
|| Value::known(F::ZERO),
)?;
region.assign_advice(
|| format!("assign rwc_inc_left {}", *offset),
self.copy_table.rwc_inc_left,
*offset,
|| Value::known(F::ZERO),
)?;
tag_chip.assign(region, *offset, &CopyDataType::Padding)?;
lt_chip.assign(region, *offset, Value::known(F::ZERO), Value::known(F::ONE))?;
*offset += 1;
Ok(())
}
}
#[derive(Clone, Debug, Default)]
pub struct ExternalData {
pub max_txs: usize,
pub max_calldata: usize,
pub txs: Vec<Transaction>,
pub max_rws: usize,
pub rws: RwMap,
pub prev_chunk_last_rw: Option<Rw>,
pub bytecodes: CodeDB,
}
#[derive(Clone, Debug, Default)]
pub struct CopyCircuit<F: Field> {
pub copy_events: Vec<CopyEvent>,
pub max_copy_rows: usize,
_marker: PhantomData<F>,
pub external_data: ExternalData,
}
impl<F: Field> CopyCircuit<F> {
pub fn new(copy_events: Vec<CopyEvent>, max_copy_rows: usize) -> Self {
Self {
copy_events,
max_copy_rows,
_marker: PhantomData,
external_data: ExternalData::default(),
}
}
pub fn new_with_external_data(
copy_events: Vec<CopyEvent>,
max_copy_rows: usize,
external_data: ExternalData,
) -> Self {
Self {
copy_events,
max_copy_rows,
_marker: PhantomData,
external_data,
}
}
pub fn new_from_block_no_external(block: &witness::Block<F>, chunk: &Chunk<F>) -> Self {
Self::new(block.copy_events.clone(), chunk.fixed_param.max_copy_rows)
}
}
impl<F: Field> SubCircuit<F> for CopyCircuit<F> {
type Config = CopyCircuitConfig<F>;
fn unusable_rows() -> usize {
6
}
fn new_from_block(block: &witness::Block<F>, chunk: &Chunk<F>) -> Self {
let chunked_copy_events = block
.copy_events
.get(chunk.chunk_context.initial_copy_index..chunk.chunk_context.end_copy_index)
.unwrap_or_default();
Self::new_with_external_data(
chunked_copy_events.to_owned(),
chunk.fixed_param.max_copy_rows,
ExternalData {
max_txs: chunk.fixed_param.max_txs,
max_calldata: chunk.fixed_param.max_calldata,
txs: block.txs.clone(),
max_rws: chunk.fixed_param.max_rws,
rws: chunk.chrono_rws.clone(),
prev_chunk_last_rw: chunk.prev_chunk_last_chrono_rw,
bytecodes: block.bytecodes.clone(),
},
)
}
fn min_num_rows_block(block: &witness::Block<F>, chunk: &Chunk<F>) -> (usize, usize) {
(
block
.copy_events
.iter()
.map(|c| c.bytes.len() * 2)
.sum::<usize>()
+ 2,
chunk.fixed_param.max_copy_rows,
)
}
fn synthesize_sub(
&self,
config: &Self::Config,
challenges: &Challenges<Value<F>>,
layouter: &mut impl Layouter<F>,
) -> Result<(), Error> {
config.assign_copy_events(layouter, &self.copy_events, self.max_copy_rows, *challenges)
}
}