Arithmetic operations involve the following steps:
Step 1: Initialize Arithmetic Table
Step 2: Generate Trace Data
Step 3: Verify Range Checks
Step 4: Validate Constraints
Step 5: Perform Circuit-Level Validation
Initialize the structure of the arithmetic table, mapping columns (e.g., operands, operation types, results) and linking them with corresponding fields in the CPU table.
1. Define Operation Types and Opcodes:
2. Define Register Mappings:
3. Generate Filters:
4. Link Arithmetic and CPU Tables:
pub fn ctl_arithmetic_rows<F: Field>() -> TableWithColumns<F> {
const COMBINED_OPS: [(usize, u32); 26] = [
// Map arithmetic operations and their corresponding opcodes.
(columns::IS_ADD, 0b100000 * (1 << 6)),
(columns::IS_ADDU, 0b100001 * (1 << 6)),
(columns::IS_ADDI, 0b001000),
// ... omit ...
];
const REGISTER_MAP: [Range<usize>; 3] = [
columns::INPUT_REGISTER_0, // input register0
columns::INPUT_REGISTER_1, // input register1
columns::OUTPUT_REGISTER, // output register
];
let filter = Some(Filter::new_simple(Column::sum(
COMBINED_OPS.iter().map(|(c, _v)| *c),
)));
TableWithColumns::new(
Table::Arithmetic,
cpu_arith_data_link(&COMBINED_OPS, ®ISTER_MAP), //Link the arithmetic table to the CPU table
filter,
)
}
Generate trace data row by row based on the input operations (Operation), supplement empty rows, and perform range checks.
1. Allocate Trace Rows:
2. Generate Trace Rows for Each Operation:
3. Fill Empty Rows:
4. Transpose to Column Data:
5. Perform Range Checks:
pub(crate) fn generate_trace(&self, operations: Vec<Operation>) -> Vec<PolynomialValues<F>> {
let max_rows = std::cmp::max(2 * operations.len(), RANGE_MAX);
let mut trace_rows = Vec::with_capacity(max_rows);
for op in operations {
let (row1, maybe_row2) = op.to_rows(); // Converts an operation to one or two rows of data.
trace_rows.push(row1);
if let Some(row2) = maybe_row2 { // If the operation requires two lines, add them to the Trace.
trace_rows.push(row2);
}
}
// Fill blank lines
let padded_len = trace_rows.len().next_power_of_two(); // Make sure the number of rows is a power of two to facilitate the interpolation and commitment operation of FRI.
for _ in trace_rows.len()..std::cmp::max(padded_len, RANGE_MAX) {
trace_rows.push(vec![F::ZERO; columns::NUM_ARITH_COLUMNS]); // Add all zero rows.
}
// Transpose to column data
let mut trace_cols = transpose(&trace_rows);
// range check
self.generate_range_checks(&mut trace_cols);
// Convert to a polynomial representation
trace_cols.into_iter().map(PolynomialValues::new).collect()
}
Validate specific columns in the trace to ensure values lie within valid ranges (e.g., ([0, 2^{16}))).
fn generate_range_checks(&self, cols: &mut [Vec<F>]) {
// Initialize the RANGE COUNTER column with the range [0, 2^16).
for i in 0..RANGE_MAX {
cols[RANGE_COUNTER][i] = F::from_canonical_usize(i);
}
for i in RANGE_MAX..cols[0].len() {
cols[RANGE_COUNTER][i] = F::from_canonical_usize(RANGE_MAX - 1);
}
// Frequency checks are performed on SHARED COLS
for col in SHARED_COLS {
for i in 0..cols[0].len() {
let x = cols[col][i].to_canonical_u64() as usize;
assert!(
x < RANGE_MAX,
"column value {} exceeds the max range value {}",
x,
RANGE_MAX
);
cols[RC_FREQUENCIES][x] += F::ONE; // statistical frequency
}
}
}
Verify constraints for different arithmetic operations to ensure inputs, outputs, and intermediate states conform to definitions. This module ensures offline validation of filled values.
1. Initialize the RANGE_COUNTER Column:
2. Count Frequencies of Shared Columns:
fn eval_packed_generic<FE, P, const D2: usize>(
&self,
vars: &Self::EvaluationFrame<FE, P, D2>,
yield_constr: &mut ConstraintConsumer<P>,
) where
FE: FieldExtension<D2, BaseField = F>,
P: PackedField<Scalar = FE>,
{
let lv: &[P; NUM_ARITH_COLUMNS] = vars.get_local_values().try_into().unwrap();
let nv: &[P; NUM_ARITH_COLUMNS] = vars.get_next_values().try_into().unwrap();
// Range checking constraints
let rc1 = lv[RANGE_COUNTER];
let rc2 = nv[RANGE_COUNTER];
yield_constr.constraint_first_row(rc1); // The first row must be 0
let incr = rc2 - rc1;
yield_constr.constraint_transition(incr * incr - incr); // Range increasing constraint
let range_max = P::Scalar::from_canonical_u64((RANGE_MAX - 1) as u64);
yield_constr.constraint_last_row(rc1 - range_max); // The last line is RANGE MAX-1
// Invocation of constraint validation for individual arithmetic operations
mul::eval_packed_generic(lv, yield_constr);
mult::eval_packed_generic(lv, yield_constr);
addcy::eval_packed_generic(lv, yield_constr);
slt::eval_packed_generic(lv, yield_constr);
lui::eval_packed_generic(lv, nv, yield_constr);
div::eval_packed(lv, nv, yield_constr);
shift::eval_packed_generic(lv, nv, yield_constr);
sra::eval_packed_generic(lv, nv, yield_constr);
lo_hi::eval_packed_generic(lv, yield_constr);
}
Extend circuit validation for all operations in the arithmetic table, ensuring constraints are satisfied. This module builds circuit constraints to construct proofs for the STARK system.
fn eval_ext_circuit(
&self,
builder: &mut CircuitBuilder<F, D>,
vars: &Self::EvaluationFrameTarget,
yield_constr: &mut RecursiveConstraintConsumer<F, D>,
) {
let lv: &[ExtensionTarget<D>; NUM_ARITH_COLUMNS] =
vars.get_local_values().try_into().unwrap();
let nv: &[ExtensionTarget<D>; NUM_ARITH_COLUMNS] =
vars.get_next_values().try_into().unwrap();
// Range Check Constraints (circuit)
let rc1 = lv[RANGE_COUNTER];
let rc2 = nv[RANGE_COUNTER];
yield_constr.constraint_first_row(builder, rc1);
let incr = builder.sub_extension(rc2, rc1);
let t = builder.mul_sub_extension(incr, incr, incr);
yield_constr.constraint_transition(builder, t);
let range_max = builder.constant_extension(F::Extension::from_canonical_usize(RANGE_MAX - 1));
let t = builder.sub_extension(rc1, range_max);
yield_constr.constraint_last_row(builder, t);
// Circuit verification that invokes individual arithmetic operations mul::eval_ext_circuit(builder, lv, yield_constr);
mult::eval_ext_circuit(builder, lv, yield_constr);
addcy::eval_ext_circuit(builder, lv, yield_constr);
slt::eval_ext_circuit(builder, lv, yield_constr);
lui::eval_ext_circuit(builder, lv, nv, yield_constr);
div::eval_ext_circuit(builder, lv, nv, yield_constr);
shift::eval_ext_circuit(builder, lv, nv, yield_constr);
sra::eval_ext_circuit(builder, lv, nv, yield_constr);
lo_hi::eval_ext_circuit(builder, lv, yield_constr);
}
The CPU’s execution primarily includes the following components:
1. Define the CpuStark structure, which is a generic structure containing the following parameters:
2. The PhantomData<F> is used as a placeholder to indicate that this structure is associated with a field type.
#[derive(Copy, Clone, Default)]
pub struct CpuStark<F, const D: usize> {
pub f: PhantomData<F>,
}
CpuStark is the core structure for CPU simulation, responsible for managing and implementing STARK-specific operations, including trace verification and constraint generation.
fn eval_packed_generic<FE, P, const D2: usize>(
&self,
vars: &Self::EvaluationFrame<FE, P, D2>,
yield_constr: &mut ConstraintConsumer<P>,
) where
FE: FieldExtension<D2, BaseField = F>,
P: PackedField<Scalar = FE>,
Step 2.1: Extract CPU State
let local_values: &[P; NUM_CPU_COLUMNS] = vars.get_local_values().try_into().unwrap();
let local_values: &CpuColumnsView<P> = local_values.borrow();
let next_values: &[P; NUM_CPU_COLUMNS] = vars.get_next_values().try_into().unwrap();
let next_values: &CpuColumnsView<P> = next_values.borrow();
bootstrap_kernel::eval_bootstrap_kernel_packed(local_values, next_values, yield_constr);
decode::eval_packed_generic(local_values, yield_constr);
jumps::eval_packed(local_values, next_values, yield_constr);
membus::eval_packed(local_values, yield_constr);
memio::eval_packed(local_values, next_values, yield_constr);
shift::eval_packed(local_values, yield_constr);
count::eval_packed(local_values, yield_constr);
syscall::eval_packed(local_values, yield_constr);
bits::eval_packed(local_values, yield_constr);
misc::eval_packed(local_values, yield_constr);
exit_kernel::eval_exit_kernel_packed(local_values, next_values, yield_constr);
Example: decode::eval_packed_generic
fn eval_packed_generic<P: PackedField>(
lv: &CpuColumnsView<P>,
yield_constr: &mut ConstraintConsumer<P>,
) {
let filter = lv.op.decode_op; // Filtering condition for instruction decoding
let opcode_check = lv.opcode_bits[0]; // Get the first bit of the opcode
yield_constr.constraint(filter * opcode_check); // Add constraint: verify the opcode is correct
}
Purpose: Verifies that the opcode is correct.
Process:
fn eval_ext_circuit(
&self,
builder: &mut CircuitBuilder<F, D>,
vars: &Self::EvaluationFrameTarget,
yield_constr: &mut RecursiveConstraintConsumer<F, D>,
)
Step 3.1: Extract State
let local_values: &[ExtensionTarget<D>; NUM_CPU_COLUMNS] =
vars.get_local_values().try_into().unwrap();
let local_values: &CpuColumnsView<ExtensionTarget<D>> = local_values.borrow();
let next_values: &[ExtensionTarget<D>; NUM_CPU_COLUMNS] =
vars.get_next_values().try_into().unwrap();
let next_values: &CpuColumnsView<ExtensionTarget<D>> = next_values.borrow();
Step 3.2: Call Module Constraint Generation
bootstrap_kernel::eval_bootstrap_kernel_ext_circuit(builder, local_values, next_values, yield_constr);
decode::eval_ext_circuit(builder, local_values, yield_constr);
jumps::eval_ext_circuit(builder, local_values, next_values, yield_constr);
membus::eval_ext_circuit(builder, local_values, yield_constr);
// ...
exit_kernel::eval_exit_kernel_ext_circuit(builder, local_values, next_values, yield_constr);
Functionality:
Example: decode::eval_ext_circuit
fn eval_ext_circuit<F: RichField + Extendable<D>, const D: usize>(
builder: &mut CircuitBuilder<F, D>,
lv: &CpuColumnsView<ExtensionTarget<D>>,
yield_constr: &mut RecursiveConstraintConsumer<F, D>,
) {
let filter = lv.op.decode_op; // Filtering condition
let opcode_check = lv.opcode_bits[0]; // First bit of the opcode
let constr = builder.mul_extension(filter, opcode_check); // Generate multiplication constraint
yield_constr.constraint(builder, constr); // Add to the constraints
}
#[test]
fn test_stark_degree() -> Result<()> {
const D: usize = 2;
type C = PoseidonGoldilocksConfig;
type F = <C as GenericConfig<D>>::F;
type S = CpuStark<F, D>;
let stark = S {
f: Default::default(),
};
test_stark_low_degree(stark)
}
Functionality:
#[test]
fn test_stark_circuit() -> Result<()> {
const D: usize = 2;
type C = PoseidonGoldilocksConfig;
type F = <C as GenericConfig<D>>::F;
type S = CpuStark<F, D>;
let stark = S {
f: Default::default(),
};
test_stark_circuit_constraints::<F, C, S, D>(stark)
}
Functionality:
Subscribe to the ZKM Blog to stay updated with the latest research by ZKM. If you have any questions about the article, you can contact the ZKM team on discord: discord.com/zkm
Arithmetic operations involve the following steps:
Step 1: Initialize Arithmetic Table
Step 2: Generate Trace Data
Step 3: Verify Range Checks
Step 4: Validate Constraints
Step 5: Perform Circuit-Level Validation
Initialize the structure of the arithmetic table, mapping columns (e.g., operands, operation types, results) and linking them with corresponding fields in the CPU table.
1. Define Operation Types and Opcodes:
2. Define Register Mappings:
3. Generate Filters:
4. Link Arithmetic and CPU Tables:
pub fn ctl_arithmetic_rows<F: Field>() -> TableWithColumns<F> {
const COMBINED_OPS: [(usize, u32); 26] = [
// Map arithmetic operations and their corresponding opcodes.
(columns::IS_ADD, 0b100000 * (1 << 6)),
(columns::IS_ADDU, 0b100001 * (1 << 6)),
(columns::IS_ADDI, 0b001000),
// ... omit ...
];
const REGISTER_MAP: [Range<usize>; 3] = [
columns::INPUT_REGISTER_0, // input register0
columns::INPUT_REGISTER_1, // input register1
columns::OUTPUT_REGISTER, // output register
];
let filter = Some(Filter::new_simple(Column::sum(
COMBINED_OPS.iter().map(|(c, _v)| *c),
)));
TableWithColumns::new(
Table::Arithmetic,
cpu_arith_data_link(&COMBINED_OPS, ®ISTER_MAP), //Link the arithmetic table to the CPU table
filter,
)
}
Generate trace data row by row based on the input operations (Operation), supplement empty rows, and perform range checks.
1. Allocate Trace Rows:
2. Generate Trace Rows for Each Operation:
3. Fill Empty Rows:
4. Transpose to Column Data:
5. Perform Range Checks:
pub(crate) fn generate_trace(&self, operations: Vec<Operation>) -> Vec<PolynomialValues<F>> {
let max_rows = std::cmp::max(2 * operations.len(), RANGE_MAX);
let mut trace_rows = Vec::with_capacity(max_rows);
for op in operations {
let (row1, maybe_row2) = op.to_rows(); // Converts an operation to one or two rows of data.
trace_rows.push(row1);
if let Some(row2) = maybe_row2 { // If the operation requires two lines, add them to the Trace.
trace_rows.push(row2);
}
}
// Fill blank lines
let padded_len = trace_rows.len().next_power_of_two(); // Make sure the number of rows is a power of two to facilitate the interpolation and commitment operation of FRI.
for _ in trace_rows.len()..std::cmp::max(padded_len, RANGE_MAX) {
trace_rows.push(vec![F::ZERO; columns::NUM_ARITH_COLUMNS]); // Add all zero rows.
}
// Transpose to column data
let mut trace_cols = transpose(&trace_rows);
// range check
self.generate_range_checks(&mut trace_cols);
// Convert to a polynomial representation
trace_cols.into_iter().map(PolynomialValues::new).collect()
}
Validate specific columns in the trace to ensure values lie within valid ranges (e.g., ([0, 2^{16}))).
fn generate_range_checks(&self, cols: &mut [Vec<F>]) {
// Initialize the RANGE COUNTER column with the range [0, 2^16).
for i in 0..RANGE_MAX {
cols[RANGE_COUNTER][i] = F::from_canonical_usize(i);
}
for i in RANGE_MAX..cols[0].len() {
cols[RANGE_COUNTER][i] = F::from_canonical_usize(RANGE_MAX - 1);
}
// Frequency checks are performed on SHARED COLS
for col in SHARED_COLS {
for i in 0..cols[0].len() {
let x = cols[col][i].to_canonical_u64() as usize;
assert!(
x < RANGE_MAX,
"column value {} exceeds the max range value {}",
x,
RANGE_MAX
);
cols[RC_FREQUENCIES][x] += F::ONE; // statistical frequency
}
}
}
Verify constraints for different arithmetic operations to ensure inputs, outputs, and intermediate states conform to definitions. This module ensures offline validation of filled values.
1. Initialize the RANGE_COUNTER Column:
2. Count Frequencies of Shared Columns:
fn eval_packed_generic<FE, P, const D2: usize>(
&self,
vars: &Self::EvaluationFrame<FE, P, D2>,
yield_constr: &mut ConstraintConsumer<P>,
) where
FE: FieldExtension<D2, BaseField = F>,
P: PackedField<Scalar = FE>,
{
let lv: &[P; NUM_ARITH_COLUMNS] = vars.get_local_values().try_into().unwrap();
let nv: &[P; NUM_ARITH_COLUMNS] = vars.get_next_values().try_into().unwrap();
// Range checking constraints
let rc1 = lv[RANGE_COUNTER];
let rc2 = nv[RANGE_COUNTER];
yield_constr.constraint_first_row(rc1); // The first row must be 0
let incr = rc2 - rc1;
yield_constr.constraint_transition(incr * incr - incr); // Range increasing constraint
let range_max = P::Scalar::from_canonical_u64((RANGE_MAX - 1) as u64);
yield_constr.constraint_last_row(rc1 - range_max); // The last line is RANGE MAX-1
// Invocation of constraint validation for individual arithmetic operations
mul::eval_packed_generic(lv, yield_constr);
mult::eval_packed_generic(lv, yield_constr);
addcy::eval_packed_generic(lv, yield_constr);
slt::eval_packed_generic(lv, yield_constr);
lui::eval_packed_generic(lv, nv, yield_constr);
div::eval_packed(lv, nv, yield_constr);
shift::eval_packed_generic(lv, nv, yield_constr);
sra::eval_packed_generic(lv, nv, yield_constr);
lo_hi::eval_packed_generic(lv, yield_constr);
}
Extend circuit validation for all operations in the arithmetic table, ensuring constraints are satisfied. This module builds circuit constraints to construct proofs for the STARK system.
fn eval_ext_circuit(
&self,
builder: &mut CircuitBuilder<F, D>,
vars: &Self::EvaluationFrameTarget,
yield_constr: &mut RecursiveConstraintConsumer<F, D>,
) {
let lv: &[ExtensionTarget<D>; NUM_ARITH_COLUMNS] =
vars.get_local_values().try_into().unwrap();
let nv: &[ExtensionTarget<D>; NUM_ARITH_COLUMNS] =
vars.get_next_values().try_into().unwrap();
// Range Check Constraints (circuit)
let rc1 = lv[RANGE_COUNTER];
let rc2 = nv[RANGE_COUNTER];
yield_constr.constraint_first_row(builder, rc1);
let incr = builder.sub_extension(rc2, rc1);
let t = builder.mul_sub_extension(incr, incr, incr);
yield_constr.constraint_transition(builder, t);
let range_max = builder.constant_extension(F::Extension::from_canonical_usize(RANGE_MAX - 1));
let t = builder.sub_extension(rc1, range_max);
yield_constr.constraint_last_row(builder, t);
// Circuit verification that invokes individual arithmetic operations mul::eval_ext_circuit(builder, lv, yield_constr);
mult::eval_ext_circuit(builder, lv, yield_constr);
addcy::eval_ext_circuit(builder, lv, yield_constr);
slt::eval_ext_circuit(builder, lv, yield_constr);
lui::eval_ext_circuit(builder, lv, nv, yield_constr);
div::eval_ext_circuit(builder, lv, nv, yield_constr);
shift::eval_ext_circuit(builder, lv, nv, yield_constr);
sra::eval_ext_circuit(builder, lv, nv, yield_constr);
lo_hi::eval_ext_circuit(builder, lv, yield_constr);
}
The CPU’s execution primarily includes the following components:
1. Define the CpuStark structure, which is a generic structure containing the following parameters:
2. The PhantomData<F> is used as a placeholder to indicate that this structure is associated with a field type.
#[derive(Copy, Clone, Default)]
pub struct CpuStark<F, const D: usize> {
pub f: PhantomData<F>,
}
CpuStark is the core structure for CPU simulation, responsible for managing and implementing STARK-specific operations, including trace verification and constraint generation.
fn eval_packed_generic<FE, P, const D2: usize>(
&self,
vars: &Self::EvaluationFrame<FE, P, D2>,
yield_constr: &mut ConstraintConsumer<P>,
) where
FE: FieldExtension<D2, BaseField = F>,
P: PackedField<Scalar = FE>,
Step 2.1: Extract CPU State
let local_values: &[P; NUM_CPU_COLUMNS] = vars.get_local_values().try_into().unwrap();
let local_values: &CpuColumnsView<P> = local_values.borrow();
let next_values: &[P; NUM_CPU_COLUMNS] = vars.get_next_values().try_into().unwrap();
let next_values: &CpuColumnsView<P> = next_values.borrow();
bootstrap_kernel::eval_bootstrap_kernel_packed(local_values, next_values, yield_constr);
decode::eval_packed_generic(local_values, yield_constr);
jumps::eval_packed(local_values, next_values, yield_constr);
membus::eval_packed(local_values, yield_constr);
memio::eval_packed(local_values, next_values, yield_constr);
shift::eval_packed(local_values, yield_constr);
count::eval_packed(local_values, yield_constr);
syscall::eval_packed(local_values, yield_constr);
bits::eval_packed(local_values, yield_constr);
misc::eval_packed(local_values, yield_constr);
exit_kernel::eval_exit_kernel_packed(local_values, next_values, yield_constr);
Example: decode::eval_packed_generic
fn eval_packed_generic<P: PackedField>(
lv: &CpuColumnsView<P>,
yield_constr: &mut ConstraintConsumer<P>,
) {
let filter = lv.op.decode_op; // Filtering condition for instruction decoding
let opcode_check = lv.opcode_bits[0]; // Get the first bit of the opcode
yield_constr.constraint(filter * opcode_check); // Add constraint: verify the opcode is correct
}
Purpose: Verifies that the opcode is correct.
Process:
fn eval_ext_circuit(
&self,
builder: &mut CircuitBuilder<F, D>,
vars: &Self::EvaluationFrameTarget,
yield_constr: &mut RecursiveConstraintConsumer<F, D>,
)
Step 3.1: Extract State
let local_values: &[ExtensionTarget<D>; NUM_CPU_COLUMNS] =
vars.get_local_values().try_into().unwrap();
let local_values: &CpuColumnsView<ExtensionTarget<D>> = local_values.borrow();
let next_values: &[ExtensionTarget<D>; NUM_CPU_COLUMNS] =
vars.get_next_values().try_into().unwrap();
let next_values: &CpuColumnsView<ExtensionTarget<D>> = next_values.borrow();
Step 3.2: Call Module Constraint Generation
bootstrap_kernel::eval_bootstrap_kernel_ext_circuit(builder, local_values, next_values, yield_constr);
decode::eval_ext_circuit(builder, local_values, yield_constr);
jumps::eval_ext_circuit(builder, local_values, next_values, yield_constr);
membus::eval_ext_circuit(builder, local_values, yield_constr);
// ...
exit_kernel::eval_exit_kernel_ext_circuit(builder, local_values, next_values, yield_constr);
Functionality:
Example: decode::eval_ext_circuit
fn eval_ext_circuit<F: RichField + Extendable<D>, const D: usize>(
builder: &mut CircuitBuilder<F, D>,
lv: &CpuColumnsView<ExtensionTarget<D>>,
yield_constr: &mut RecursiveConstraintConsumer<F, D>,
) {
let filter = lv.op.decode_op; // Filtering condition
let opcode_check = lv.opcode_bits[0]; // First bit of the opcode
let constr = builder.mul_extension(filter, opcode_check); // Generate multiplication constraint
yield_constr.constraint(builder, constr); // Add to the constraints
}
#[test]
fn test_stark_degree() -> Result<()> {
const D: usize = 2;
type C = PoseidonGoldilocksConfig;
type F = <C as GenericConfig<D>>::F;
type S = CpuStark<F, D>;
let stark = S {
f: Default::default(),
};
test_stark_low_degree(stark)
}
Functionality:
#[test]
fn test_stark_circuit() -> Result<()> {
const D: usize = 2;
type C = PoseidonGoldilocksConfig;
type F = <C as GenericConfig<D>>::F;
type S = CpuStark<F, D>;
let stark = S {
f: Default::default(),
};
test_stark_circuit_constraints::<F, C, S, D>(stark)
}
Functionality:
Subscribe to the ZKM Blog to stay updated with the latest research by ZKM. If you have any questions about the article, you can contact the ZKM team on discord: discord.com/zkm