diff --git a/evm/spec/zkevm.pdf b/evm/spec/zkevm.pdf index 97217d5d80..3b10fba30b 100644 Binary files a/evm/spec/zkevm.pdf and b/evm/spec/zkevm.pdf differ diff --git a/evm/src/all_stark.rs b/evm/src/all_stark.rs index a2996eb47b..cf03a60088 100644 --- a/evm/src/all_stark.rs +++ b/evm/src/all_stark.rs @@ -135,6 +135,11 @@ fn ctl_byte_packing() -> CrossTableLookup { cpu_stark::ctl_data_byte_packing_push(), Some(cpu_stark::ctl_filter_byte_packing_push()), ); + let cpu_jumptable_read_looking = TableWithColumns::new( + Table::Cpu, + cpu_stark::ctl_data_jumptable_read(), + Some(cpu_stark::ctl_filter_syscall_exceptions()), + ); let byte_packing_looked = TableWithColumns::new( Table::BytePacking, byte_packing_stark::ctl_looked_data(), @@ -145,6 +150,7 @@ fn ctl_byte_packing() -> CrossTableLookup { cpu_packing_looking, cpu_unpacking_looking, cpu_push_packing_looking, + cpu_jumptable_read_looking, ], byte_packing_looked, ) @@ -238,6 +244,16 @@ fn ctl_memory() -> CrossTableLookup { cpu_stark::ctl_data_partial_memory::(), Some(cpu_stark::ctl_filter_partial_memory()), ); + let cpu_set_context_write = TableWithColumns::new( + Table::Cpu, + cpu_stark::ctl_data_memory_old_sp_write_set_context::(), + Some(cpu_stark::ctl_filter_set_context()), + ); + let cpu_set_context_read = TableWithColumns::new( + Table::Cpu, + cpu_stark::ctl_data_memory_new_sp_read_set_context::(), + Some(cpu_stark::ctl_filter_set_context()), + ); let keccak_sponge_reads = (0..KECCAK_RATE_BYTES).map(|i| { TableWithColumns::new( Table::KeccakSponge, @@ -252,12 +268,17 @@ fn ctl_memory() -> CrossTableLookup { Some(byte_packing_stark::ctl_looking_memory_filter(i)), ) }); - let all_lookers = iter::once(cpu_memory_code_read) - .chain(cpu_memory_gp_ops) - .chain(iter::once(cpu_push_write_ops)) - .chain(keccak_sponge_reads) - .chain(byte_packing_ops) - .collect(); + let all_lookers = vec![ + cpu_memory_code_read, + cpu_push_write_ops, + cpu_set_context_write, + cpu_set_context_read, + ] + .into_iter() + .chain(cpu_memory_gp_ops) + .chain(keccak_sponge_reads) + .chain(byte_packing_ops) + .collect(); let memory_looked = TableWithColumns::new( Table::Memory, memory_stark::ctl_data(), diff --git a/evm/src/cpu/contextops.rs b/evm/src/cpu/contextops.rs index 77debc759c..7f46d1418f 100644 --- a/evm/src/cpu/contextops.rs +++ b/evm/src/cpu/contextops.rs @@ -7,11 +7,12 @@ use plonky2::iop::ext_target::ExtensionTarget; use plonky2::plonk::circuit_builder::CircuitBuilder; use super::columns::ops::OpsColumnsView; +use super::cpu_stark::{disable_unused_channels, disable_unused_channels_circuit}; use super::membus::NUM_GP_CHANNELS; use crate::constraint_consumer::{ConstraintConsumer, RecursiveConstraintConsumer}; use crate::cpu::columns::CpuColumnsView; use crate::cpu::kernel::constants::context_metadata::ContextMetadata; -use crate::memory::segments::{Segment, SEGMENT_SCALING_FACTOR}; +use crate::memory::segments::Segment; use crate::memory::VALUE_LIMBS; // If true, the instruction will keep the current context for the next row. @@ -95,12 +96,7 @@ fn eval_packed_get( yield_constr.constraint(filter * (nv.stack_len - (lv.stack_len + P::ONES))); // Unused channels. - for i in 1..NUM_GP_CHANNELS { - if i != 3 { - let channel = lv.mem_channels[i]; - yield_constr.constraint(filter * channel.used); - } - } + disable_unused_channels(lv, filter, vec![1], yield_constr); yield_constr.constraint(filter * nv.mem_channels[0].used); } @@ -137,13 +133,7 @@ fn eval_ext_circuit_get, const D: usize>( } // Unused channels. - for i in 1..NUM_GP_CHANNELS { - if i != 3 { - let channel = lv.mem_channels[i]; - let constr = builder.mul_extension(filter, channel.used); - yield_constr.constraint(builder, constr); - } - } + disable_unused_channels_circuit(builder, lv, filter, vec![1], yield_constr); { let constr = builder.mul_extension(filter, nv.mem_channels[0].used); yield_constr.constraint(builder, constr); @@ -158,12 +148,6 @@ fn eval_packed_set( ) { let filter = lv.op.context_op * lv.opcode_bits[0]; let stack_top = lv.mem_channels[0].value; - let write_old_sp_channel = lv.mem_channels[1]; - let read_new_sp_channel = lv.mem_channels[2]; - // We need to unscale the context metadata segment and related field. - let ctx_metadata_segment = P::Scalar::from_canonical_usize(Segment::ContextMetadata.unscale()); - let stack_size_field = P::Scalar::from_canonical_usize(ContextMetadata::StackSize.unscale()); - let local_sp_dec = lv.stack_len - P::ONES; // The next row's context is read from stack_top. yield_constr.constraint(filter * (stack_top[2] - nv.context)); @@ -171,27 +155,9 @@ fn eval_packed_set( yield_constr.constraint(filter * limb); } - // The old SP is decremented (since the new context was popped) and written to memory. - yield_constr.constraint(filter * (write_old_sp_channel.value[0] - local_sp_dec)); - for &limb in &write_old_sp_channel.value[1..] { - yield_constr.constraint(filter * limb); - } - yield_constr.constraint(filter * (write_old_sp_channel.used - P::ONES)); - yield_constr.constraint(filter * write_old_sp_channel.is_read); - yield_constr.constraint(filter * (write_old_sp_channel.addr_context - lv.context)); - yield_constr.constraint(filter * (write_old_sp_channel.addr_segment - ctx_metadata_segment)); - yield_constr.constraint(filter * (write_old_sp_channel.addr_virtual - stack_size_field)); - + // The old SP is decremented (since the new context was popped) and stored in memory. // The new SP is loaded from memory. - yield_constr.constraint(filter * (read_new_sp_channel.value[0] - nv.stack_len)); - for &limb in &read_new_sp_channel.value[1..] { - yield_constr.constraint(filter * limb); - } - yield_constr.constraint(filter * (read_new_sp_channel.used - P::ONES)); - yield_constr.constraint(filter * (read_new_sp_channel.is_read - P::ONES)); - yield_constr.constraint(filter * (read_new_sp_channel.addr_context - nv.context)); - yield_constr.constraint(filter * (read_new_sp_channel.addr_segment - ctx_metadata_segment)); - yield_constr.constraint(filter * (read_new_sp_channel.addr_virtual - stack_size_field)); + // This is all done with CTLs: nothing is constrained here. // Constrain stack_inv_aux_2. let new_top_channel = nv.mem_channels[0]; @@ -200,17 +166,19 @@ fn eval_packed_set( * (lv.general.stack().stack_inv_aux * lv.opcode_bits[0] - lv.general.stack().stack_inv_aux_2), ); - // The new top is loaded in memory channel 3, if the stack isn't empty (see eval_packed). + // The new top is loaded in memory channel 2, if the stack isn't empty (see eval_packed). for (&limb_new_top, &limb_read_top) in new_top_channel .value .iter() - .zip(lv.mem_channels[3].value.iter()) + .zip(lv.mem_channels[2].value.iter()) { yield_constr.constraint( lv.op.context_op * lv.general.stack().stack_inv_aux_2 * (limb_new_top - limb_read_top), ); } + // Unused channels. + disable_unused_channels(lv, filter, vec![1], yield_constr); yield_constr.constraint(filter * new_top_channel.used); } @@ -224,17 +192,6 @@ fn eval_ext_circuit_set, const D: usize>( ) { let filter = builder.mul_extension(lv.op.context_op, lv.opcode_bits[0]); let stack_top = lv.mem_channels[0].value; - let write_old_sp_channel = lv.mem_channels[1]; - let read_new_sp_channel = lv.mem_channels[2]; - // We need to unscale the context metadata segment and related field. - let ctx_metadata_segment = builder.constant_extension(F::Extension::from_canonical_usize( - Segment::ContextMetadata.unscale(), - )); - let stack_size_field = builder.constant_extension(F::Extension::from_canonical_usize( - ContextMetadata::StackSize.unscale(), - )); - let one = builder.one_extension(); - let local_sp_dec = builder.sub_extension(lv.stack_len, one); // The next row's context is read from stack_top. { @@ -247,73 +204,9 @@ fn eval_ext_circuit_set, const D: usize>( yield_constr.constraint(builder, constr); } - // The old SP is decremented (since the new context was popped) and written to memory. - { - let diff = builder.sub_extension(write_old_sp_channel.value[0], local_sp_dec); - let constr = builder.mul_extension(filter, diff); - yield_constr.constraint(builder, constr); - } - for &limb in &write_old_sp_channel.value[1..] { - let constr = builder.mul_extension(filter, limb); - yield_constr.constraint(builder, constr); - } - { - let constr = builder.mul_sub_extension(filter, write_old_sp_channel.used, filter); - yield_constr.constraint(builder, constr); - } - { - let constr = builder.mul_extension(filter, write_old_sp_channel.is_read); - yield_constr.constraint(builder, constr); - } - { - let diff = builder.sub_extension(write_old_sp_channel.addr_context, lv.context); - let constr = builder.mul_extension(filter, diff); - yield_constr.constraint(builder, constr); - } - { - let diff = builder.sub_extension(write_old_sp_channel.addr_segment, ctx_metadata_segment); - let constr = builder.mul_extension(filter, diff); - yield_constr.constraint(builder, constr); - } - { - let diff = builder.sub_extension(write_old_sp_channel.addr_virtual, stack_size_field); - let constr = builder.mul_extension(filter, diff); - yield_constr.constraint(builder, constr); - } - + // The old SP is decremented (since the new context was popped) and stored in memory. // The new SP is loaded from memory. - { - let diff = builder.sub_extension(read_new_sp_channel.value[0], nv.stack_len); - let constr = builder.mul_extension(filter, diff); - yield_constr.constraint(builder, constr); - } - for &limb in &read_new_sp_channel.value[1..] { - let constr = builder.mul_extension(filter, limb); - yield_constr.constraint(builder, constr); - } - { - let constr = builder.mul_sub_extension(filter, read_new_sp_channel.used, filter); - yield_constr.constraint(builder, constr); - } - { - let constr = builder.mul_sub_extension(filter, read_new_sp_channel.is_read, filter); - yield_constr.constraint(builder, constr); - } - { - let diff = builder.sub_extension(read_new_sp_channel.addr_context, nv.context); - let constr = builder.mul_extension(filter, diff); - yield_constr.constraint(builder, constr); - } - { - let diff = builder.sub_extension(read_new_sp_channel.addr_segment, ctx_metadata_segment); - let constr = builder.mul_extension(filter, diff); - yield_constr.constraint(builder, constr); - } - { - let diff = builder.sub_extension(read_new_sp_channel.addr_virtual, stack_size_field); - let constr = builder.mul_extension(filter, diff); - yield_constr.constraint(builder, constr); - } + // This is all done with CTLs: nothing is constrained here. // Constrain stack_inv_aux_2. let new_top_channel = nv.mem_channels[0]; @@ -326,11 +219,11 @@ fn eval_ext_circuit_set, const D: usize>( let constr = builder.mul_extension(lv.op.context_op, diff); yield_constr.constraint(builder, constr); } - // The new top is loaded in memory channel 3, if the stack isn't empty (see eval_packed). + // The new top is loaded in memory channel 2, if the stack isn't empty (see eval_packed). for (&limb_new_top, &limb_read_top) in new_top_channel .value .iter() - .zip(lv.mem_channels[3].value.iter()) + .zip(lv.mem_channels[2].value.iter()) { let diff = builder.sub_extension(limb_new_top, limb_read_top); let prod = builder.mul_extension(lv.general.stack().stack_inv_aux_2, diff); @@ -338,6 +231,8 @@ fn eval_ext_circuit_set, const D: usize>( yield_constr.constraint(builder, constr); } + // Unused channels. + disable_unused_channels_circuit(builder, lv, filter, vec![1], yield_constr); { let constr = builder.mul_extension(filter, new_top_channel.used); yield_constr.constraint(builder, constr); @@ -355,10 +250,10 @@ pub(crate) fn eval_packed( eval_packed_set(lv, nv, yield_constr); // Stack constraints. - // Both operations use memory channel 3. The operations are similar enough that + // Both operations use memory channel 2. The operations are similar enough that // we can constrain both at the same time. let filter = lv.op.context_op; - let channel = lv.mem_channels[3]; + let channel = lv.mem_channels[2]; // For get_context, we check if lv.stack_len is 0. For set_context, we check if nv.stack_len is 0. // However, for get_context, we can deduce lv.stack_len from nv.stack_len since the operation only pushes. let stack_len = nv.stack_len - (P::ONES - lv.opcode_bits[0]); @@ -396,10 +291,10 @@ pub(crate) fn eval_ext_circuit, const D: usize>( eval_ext_circuit_set(builder, lv, nv, yield_constr); // Stack constraints. - // Both operations use memory channel 3. The operations are similar enough that + // Both operations use memory channel 2. The operations are similar enough that // we can constrain both at the same time. let filter = lv.op.context_op; - let channel = lv.mem_channels[3]; + let channel = lv.mem_channels[2]; // For get_context, we check if lv.stack_len is 0. For set_context, we check if nv.stack_len is 0. // However, for get_context, we can deduce lv.stack_len from nv.stack_len since the operation only pushes. let diff = builder.add_const_extension(lv.opcode_bits[0], -F::ONE); diff --git a/evm/src/cpu/cpu_stark.rs b/evm/src/cpu/cpu_stark.rs index 478dc9c2a6..1212e7a741 100644 --- a/evm/src/cpu/cpu_stark.rs +++ b/evm/src/cpu/cpu_stark.rs @@ -11,6 +11,7 @@ use plonky2::iop::ext_target::ExtensionTarget; use super::columns::CpuColumnsView; use super::halt; +use super::kernel::constants::context_metadata::ContextMetadata; use super::membus::NUM_GP_CHANNELS; use crate::all_stark::Table; use crate::constraint_consumer::{ConstraintConsumer, RecursiveConstraintConsumer}; @@ -21,7 +22,7 @@ use crate::cpu::{ }; use crate::cross_table_lookup::{Column, Filter, TableWithColumns}; use crate::evaluation_frame::{StarkEvaluationFrame, StarkFrame}; -use crate::memory::segments::{Segment, SEGMENT_SCALING_FACTOR}; +use crate::memory::segments::Segment; use crate::memory::{NUM_CHANNELS, VALUE_LIMBS}; use crate::stark::Stark; @@ -189,6 +190,40 @@ pub(crate) fn ctl_filter_byte_unpacking() -> Filter { ) } +/// Creates the vector of `Columns` corresponding to three consecutive (byte) reads in memory. +/// It's used by syscalls and exceptions to read an address in a jumptable. +pub(crate) fn ctl_data_jumptable_read() -> Vec> { + let is_read = Column::constant(F::ONE); + let mut res = vec![is_read]; + + // When reading the jumptable, the address to start reading from is in + // GP channel 1; the result is in GP channel 1's values. + let channel_map = COL_MAP.mem_channels[1]; + res.extend(Column::singles([ + channel_map.addr_context, + channel_map.addr_segment, + channel_map.addr_virtual, + ])); + let val = Column::singles(channel_map.value); + + // len is always 3. + let len = Column::constant(F::from_canonical_usize(3)); + res.push(len); + + let num_channels = F::from_canonical_usize(NUM_CHANNELS); + let timestamp = Column::linear_combination([(COL_MAP.clock, num_channels)]); + res.push(timestamp); + + res.extend(val); + + res +} + +/// CTL filter for syscalls and exceptions. +pub(crate) fn ctl_filter_syscall_exceptions() -> Filter { + Filter::new_simple(Column::sum([COL_MAP.op.syscall, COL_MAP.op.exception])) +} + /// Creates the vector of `Columns` corresponding to the contents of the CPU registers when performing a `PUSH`. /// `PUSH` internal reads are done by calling `BytePackingStark`. pub(crate) fn ctl_data_byte_packing_push() -> Vec> { @@ -305,6 +340,53 @@ pub(crate) fn ctl_data_partial_memory() -> Vec> { cols } +/// Old stack pointer write for SET_CONTEXT. +pub(crate) fn ctl_data_memory_old_sp_write_set_context() -> Vec> { + let mut cols = vec![ + Column::constant(F::ZERO), // is_read + Column::single(COL_MAP.context), // addr_context + Column::constant(F::from_canonical_usize(Segment::ContextMetadata.unscale())), // addr_segment + Column::constant(F::from_canonical_usize( + ContextMetadata::StackSize.unscale(), + )), // addr_virtual + ]; + + // Low limb is current stack length minus one. + cols.push(Column::linear_combination_with_constant( + [(COL_MAP.stack_len, F::ONE)], + -F::ONE, + )); + + // High limbs of the value are all zero. + cols.extend(repeat(Column::constant(F::ZERO)).take(VALUE_LIMBS - 1)); + + cols.push(mem_time_and_channel(MEM_GP_CHANNELS_IDX_START + 1)); + + cols +} + +/// New stack pointer read for SET_CONTEXT. +pub(crate) fn ctl_data_memory_new_sp_read_set_context() -> Vec> { + let mut cols = vec![ + Column::constant(F::ONE), // is_read + Column::single(COL_MAP.mem_channels[0].value[2]), // addr_context (in the top of the stack) + Column::constant(F::from_canonical_usize(Segment::ContextMetadata.unscale())), // addr_segment + Column::constant(F::from_canonical_u64( + ContextMetadata::StackSize as u64 - Segment::ContextMetadata as u64, + )), // addr_virtual + ]; + + // Low limb is new stack length. + cols.push(Column::single_next_row(COL_MAP.stack_len)); + + // High limbs of the value are all zero. + cols.extend(repeat(Column::constant(F::ZERO)).take(VALUE_LIMBS - 1)); + + cols.push(mem_time_and_channel(MEM_GP_CHANNELS_IDX_START + 2)); + + cols +} + /// CTL filter for code read and write operations. pub(crate) fn ctl_filter_code_memory() -> Filter { Filter::new_simple(Column::sum(COL_MAP.op.iter())) @@ -319,6 +401,49 @@ pub(crate) fn ctl_filter_partial_memory() -> Filter { Filter::new_simple(Column::single(COL_MAP.partial_channel.used)) } +/// CTL filter for the `SET_CONTEXT` operation. +/// SET_CONTEXT is differentiated from GET_CONTEXT by its zeroth bit set to 1 +pub(crate) fn ctl_filter_set_context() -> Filter { + Filter::new( + vec![( + Column::single(COL_MAP.op.context_op), + Column::single(COL_MAP.opcode_bits[0]), + )], + vec![], + ) +} + +/// Disable the specified memory channels. +/// Since channel 0 contains the top of the stack and is handled specially, +/// channels to disable are 1, 2 or both. All cases can be expressed as a vec. +pub(crate) fn disable_unused_channels( + lv: &CpuColumnsView

, + filter: P, + channels: Vec, + yield_constr: &mut ConstraintConsumer

, +) { + for i in channels { + yield_constr.constraint(filter * lv.mem_channels[i].used); + } +} + +/// Circuit version of `disable_unused_channels`. +/// Disable the specified memory channels. +/// Since channel 0 contains the top of the stack and is handled specially, +/// channels to disable are 1, 2 or both. All cases can be expressed as a vec. +pub(crate) fn disable_unused_channels_circuit, const D: usize>( + builder: &mut plonky2::plonk::circuit_builder::CircuitBuilder, + lv: &CpuColumnsView>, + filter: ExtensionTarget, + channels: Vec, + yield_constr: &mut RecursiveConstraintConsumer, +) { + for i in channels { + let constr = builder.mul_extension(filter, lv.mem_channels[i].used); + yield_constr.constraint(builder, constr); + } +} + /// Structure representing the CPU Stark. #[derive(Copy, Clone, Default)] pub(crate) struct CpuStark { diff --git a/evm/src/cpu/dup_swap.rs b/evm/src/cpu/dup_swap.rs index 44763fdc91..7b9d80f37c 100644 --- a/evm/src/cpu/dup_swap.rs +++ b/evm/src/cpu/dup_swap.rs @@ -138,11 +138,6 @@ fn eval_packed_dup( // Disable next top. yield_constr.constraint(filter * nv.mem_channels[0].used); - - // Constrain unused channels. - for i in 3..NUM_GP_CHANNELS { - yield_constr.constraint(filter * lv.mem_channels[i].used); - } } /// Circuit version of `eval_packed_dup`. @@ -205,12 +200,6 @@ fn eval_ext_circuit_dup, const D: usize>( let constr = builder.mul_extension(filter, nv.mem_channels[0].used); yield_constr.constraint(builder, constr); } - - // Constrain unused channels. - for i in 3..NUM_GP_CHANNELS { - let constr = builder.mul_extension(filter, lv.mem_channels[i].used); - yield_constr.constraint(builder, constr); - } } /// Evaluates constraints for SWAP. @@ -245,11 +234,6 @@ fn eval_packed_swap( // Disable next top. yield_constr.constraint(filter * nv.mem_channels[0].used); - - // Constrain unused channels. - for i in 3..NUM_GP_CHANNELS { - yield_constr.constraint(filter * lv.mem_channels[i].used); - } } /// Circuit version of `eval_packed_swap`. @@ -314,12 +298,6 @@ fn eval_ext_circuit_swap, const D: usize>( let constr = builder.mul_extension(filter, nv.mem_channels[0].used); yield_constr.constraint(builder, constr); } - - // Constrain unused channels. - for i in 3..NUM_GP_CHANNELS { - let constr = builder.mul_extension(filter, lv.mem_channels[i].used); - yield_constr.constraint(builder, constr); - } } /// Evaluates the constraints for the DUP and SWAP opcodes. diff --git a/evm/src/cpu/kernel/tests/add11.rs b/evm/src/cpu/kernel/tests/add11.rs index 89cdf82d66..c2725488a6 100644 --- a/evm/src/cpu/kernel/tests/add11.rs +++ b/evm/src/cpu/kernel/tests/add11.rs @@ -16,7 +16,7 @@ use crate::cpu::kernel::tests::account_code::initialize_mpts; use crate::generation::mpt::{AccountRlp, LegacyReceiptRlp}; use crate::generation::rlp::all_rlp_prover_inputs_reversed; use crate::generation::TrieInputs; -use crate::memory::segments::{Segment, SEGMENT_SCALING_FACTOR}; +use crate::memory::segments::Segment; use crate::proof::{BlockHashes, BlockMetadata, TrieRoots}; use crate::util::h2u; use crate::GenerationInputs; diff --git a/evm/src/cpu/membus.rs b/evm/src/cpu/membus.rs index e86d1ce554..fb6c530113 100644 --- a/evm/src/cpu/membus.rs +++ b/evm/src/cpu/membus.rs @@ -7,7 +7,7 @@ use crate::constraint_consumer::{ConstraintConsumer, RecursiveConstraintConsumer use crate::cpu::columns::CpuColumnsView; /// General-purpose memory channels; they can read and write to all contexts/segments/addresses. -pub(crate) const NUM_GP_CHANNELS: usize = 4; +pub(crate) const NUM_GP_CHANNELS: usize = 3; /// Indices for code and general purpose memory channels. pub mod channel_indices { @@ -29,8 +29,11 @@ pub mod channel_indices { /// - the address is `program_counter`, /// - the value must fit in one byte (in the least-significant position) and its eight bits are /// found in `opcode_bits`. +/// +/// There is also a partial channel, which shares its values with another general purpose channel. +/// /// These limitations save us numerous columns in the CPU table. -pub(crate) const NUM_CHANNELS: usize = channel_indices::GP.end; +pub(crate) const NUM_CHANNELS: usize = channel_indices::GP.end + 1; /// Evaluates constraints regarding the membus. pub(crate) fn eval_packed( diff --git a/evm/src/cpu/memio.rs b/evm/src/cpu/memio.rs index 7cb4c38855..924f030f5f 100644 --- a/evm/src/cpu/memio.rs +++ b/evm/src/cpu/memio.rs @@ -9,7 +9,7 @@ use super::cpu_stark::get_addr; use crate::constraint_consumer::{ConstraintConsumer, RecursiveConstraintConsumer}; use crate::cpu::columns::CpuColumnsView; use crate::cpu::stack; -use crate::memory::segments::{Segment, SEGMENT_SCALING_FACTOR}; +use crate::memory::segments::Segment; const fn get_addr_load(lv: &CpuColumnsView) -> (T, T, T) { get_addr(lv, 0) diff --git a/evm/src/cpu/shift.rs b/evm/src/cpu/shift.rs index 29baa5ea63..9e751421ff 100644 --- a/evm/src/cpu/shift.rs +++ b/evm/src/cpu/shift.rs @@ -48,7 +48,7 @@ pub(crate) fn eval_packed( yield_constr.constraint(is_shift * (two_exp.addr_virtual - displacement.value[0])); // Other channels must be unused - for chan in &lv.mem_channels[3..NUM_GP_CHANNELS - 1] { + for chan in &lv.mem_channels[3..NUM_GP_CHANNELS] { yield_constr.constraint(is_shift * chan.used); // channel is not used } @@ -116,7 +116,7 @@ pub(crate) fn eval_ext_circuit, const D: usize>( yield_constr.constraint(builder, t); // Other channels must be unused - for chan in &lv.mem_channels[3..NUM_GP_CHANNELS - 1] { + for chan in &lv.mem_channels[3..NUM_GP_CHANNELS] { let t = builder.mul_extension(is_shift, chan.used); yield_constr.constraint(builder, t); } diff --git a/evm/src/cpu/syscalls_exceptions.rs b/evm/src/cpu/syscalls_exceptions.rs index 337a0a86ec..debae9dcd5 100644 --- a/evm/src/cpu/syscalls_exceptions.rs +++ b/evm/src/cpu/syscalls_exceptions.rs @@ -17,7 +17,6 @@ use crate::memory::segments::Segment; // Copy the constant but make it `usize`. const BYTES_PER_OFFSET: usize = crate::cpu::kernel::assembler::BYTES_PER_OFFSET as usize; -const_assert!(BYTES_PER_OFFSET < NUM_GP_CHANNELS); // Reserve one channel for stack push /// Evaluates constraints for syscalls and exceptions. pub(crate) fn eval_packed( @@ -71,41 +70,38 @@ pub(crate) fn eval_packed( let exc_handler_addr_start = exc_jumptable_start + exc_code * P::Scalar::from_canonical_usize(BYTES_PER_OFFSET); - for (i, channel) in lv.mem_channels[1..BYTES_PER_OFFSET + 1].iter().enumerate() { - // Set `used` and `is_read`. - yield_constr.constraint(total_filter * (channel.used - P::ONES)); - yield_constr.constraint(total_filter * (channel.is_read - P::ONES)); + let jumpdest_channel = lv.mem_channels[1]; - // Set kernel context and code segment - yield_constr.constraint(total_filter * channel.addr_context); - yield_constr.constraint(total_filter * (channel.addr_segment - code_segment)); + // Set `used` and `is_read`. + // The channel is not used: the reads will be done with the byte packing CTL. + yield_constr.constraint(total_filter * (jumpdest_channel.used)); + yield_constr.constraint(total_filter * (jumpdest_channel.is_read - P::ONES)); - // Set address, using a separate channel for each of the `BYTES_PER_OFFSET` limbs. - let limb_address_syscall = opcode_handler_addr_start + P::Scalar::from_canonical_usize(i); - let limb_address_exception = exc_handler_addr_start + P::Scalar::from_canonical_usize(i); + // Set kernel context and code segment + yield_constr.constraint(total_filter * jumpdest_channel.addr_context); + yield_constr.constraint(total_filter * (jumpdest_channel.addr_segment - code_segment)); - yield_constr.constraint(filter_syscall * (channel.addr_virtual - limb_address_syscall)); - yield_constr.constraint(filter_exception * (channel.addr_virtual - limb_address_exception)); + // Set address. + yield_constr + .constraint(filter_syscall * (jumpdest_channel.addr_virtual - opcode_handler_addr_start)); + yield_constr + .constraint(filter_exception * (jumpdest_channel.addr_virtual - exc_handler_addr_start)); + + // Set higher limbs to zero. + for &limb in &jumpdest_channel.value[1..] { + yield_constr.constraint(total_filter * limb); } // Disable unused channels - for channel in &lv.mem_channels[BYTES_PER_OFFSET + 1..NUM_GP_CHANNELS] { + for channel in &lv.mem_channels[2..NUM_GP_CHANNELS] { yield_constr.constraint(total_filter * channel.used); } // Set program counter to the handler address - // The addresses are big-endian in memory - let target = lv.mem_channels[1..BYTES_PER_OFFSET + 1] - .iter() - .map(|channel| channel.value[0]) - .fold(P::ZEROS, |cumul, limb| { - cumul * P::Scalar::from_canonical_u64(256) + limb - }); - yield_constr.constraint_transition(total_filter * (nv.program_counter - target)); + yield_constr + .constraint_transition(total_filter * (nv.program_counter - jumpdest_channel.value[0])); // Set kernel mode yield_constr.constraint_transition(total_filter * (nv.is_kernel_mode - P::ONES)); - // Maintain current context - yield_constr.constraint_transition(total_filter * (nv.context - lv.context)); // Reset gas counter to zero. yield_constr.constraint_transition(total_filter * nv.gas); @@ -197,61 +193,58 @@ pub(crate) fn eval_ext_circuit, const D: usize>( exc_jumptable_start, ); - for (i, channel) in lv.mem_channels[1..BYTES_PER_OFFSET + 1].iter().enumerate() { - // Set `used` and `is_read`. - { - let constr = builder.mul_sub_extension(total_filter, channel.used, total_filter); - yield_constr.constraint(builder, constr); - } - { - let constr = builder.mul_sub_extension(total_filter, channel.is_read, total_filter); - yield_constr.constraint(builder, constr); - } - - // Set kernel context and code segment - { - let constr = builder.mul_extension(total_filter, channel.addr_context); - yield_constr.constraint(builder, constr); - } - { - let constr = builder.arithmetic_extension( - F::ONE, - -code_segment, - total_filter, - channel.addr_segment, - total_filter, - ); - yield_constr.constraint(builder, constr); - } - - // Set address, using a separate channel for each of the `BYTES_PER_OFFSET` limbs. - { - let diff_syscall = - builder.sub_extension(channel.addr_virtual, opcode_handler_addr_start); - let constr = builder.arithmetic_extension( - F::ONE, - -F::from_canonical_usize(i), - filter_syscall, - diff_syscall, - filter_syscall, - ); - yield_constr.constraint(builder, constr); - - let diff_exception = - builder.sub_extension(channel.addr_virtual, exc_handler_addr_start); - let constr = builder.arithmetic_extension( - F::ONE, - -F::from_canonical_usize(i), - filter_exception, - diff_exception, - filter_exception, - ); - yield_constr.constraint(builder, constr); - } + let jumpdest_channel = lv.mem_channels[1]; + + // Set `used` and `is_read`. + // The channel is not used: the reads will be done with the byte packing CTL. + { + let constr = builder.mul_extension(total_filter, jumpdest_channel.used); + yield_constr.constraint(builder, constr); + } + { + let constr = + builder.mul_sub_extension(total_filter, jumpdest_channel.is_read, total_filter); + yield_constr.constraint(builder, constr); + } + + // Set kernel context and code segment + { + let constr = builder.mul_extension(total_filter, jumpdest_channel.addr_context); + yield_constr.constraint(builder, constr); + } + { + let constr = builder.arithmetic_extension( + F::ONE, + -code_segment, + total_filter, + jumpdest_channel.addr_segment, + total_filter, + ); + yield_constr.constraint(builder, constr); + } + + // Set address. + { + let diff_syscall = + builder.sub_extension(jumpdest_channel.addr_virtual, opcode_handler_addr_start); + let constr = builder.mul_extension((filter_syscall), diff_syscall); + yield_constr.constraint(builder, constr); + } + { + let diff_exception = + builder.sub_extension(jumpdest_channel.addr_virtual, exc_handler_addr_start); + let constr = builder.mul_extension(filter_exception, diff_exception); + yield_constr.constraint(builder, constr); } - // Disable unused channels (the last channel is used to push to the stack) - for channel in &lv.mem_channels[BYTES_PER_OFFSET + 1..NUM_GP_CHANNELS] { + // Set higher limbs to zero. + for &limb in &jumpdest_channel.value[1..] { + let constr = builder.mul_extension(total_filter, limb); + yield_constr.constraint(builder, constr); + } + + // Disable unused channels + for channel in &lv.mem_channels[2..NUM_GP_CHANNELS] { let constr = builder.mul_extension(total_filter, channel.used); yield_constr.constraint(builder, constr); } @@ -259,13 +252,7 @@ pub(crate) fn eval_ext_circuit, const D: usize>( // Set program counter to the handler address // The addresses are big-endian in memory { - let target = lv.mem_channels[1..BYTES_PER_OFFSET + 1] - .iter() - .map(|channel| channel.value[0]) - .fold(builder.zero_extension(), |cumul, limb| { - builder.mul_const_add_extension(F::from_canonical_u64(256), cumul, limb) - }); - let diff = builder.sub_extension(nv.program_counter, target); + let diff = builder.sub_extension(nv.program_counter, jumpdest_channel.value[0]); let constr = builder.mul_extension(total_filter, diff); yield_constr.constraint_transition(builder, constr); } @@ -274,12 +261,6 @@ pub(crate) fn eval_ext_circuit, const D: usize>( let constr = builder.mul_sub_extension(total_filter, nv.is_kernel_mode, total_filter); yield_constr.constraint_transition(builder, constr); } - // Maintain current context - { - let diff = builder.sub_extension(nv.context, lv.context); - let constr = builder.mul_extension(total_filter, diff); - yield_constr.constraint_transition(builder, constr); - } // Reset gas counter to zero. { let constr = builder.mul_extension(total_filter, nv.gas); diff --git a/evm/src/witness/operation.rs b/evm/src/witness/operation.rs index c9dea2307b..e87c1465e1 100644 --- a/evm/src/witness/operation.rs +++ b/evm/src/witness/operation.rs @@ -4,8 +4,8 @@ use keccak_hash::keccak; use plonky2::field::types::Field; use super::util::{ - byte_packing_log, byte_unpacking_log, mem_write_partial_log_and_fill, push_no_write, - push_with_write, + byte_packing_log, byte_unpacking_log, mem_read_with_log, mem_write_log, + mem_write_partial_log_and_fill, push_no_write, push_with_write, }; use crate::arithmetic::BinaryOperator; use crate::cpu::columns::CpuColumnsView; @@ -349,7 +349,7 @@ pub(crate) fn generate_get_context( Segment::Stack, state.registers.stack_len - 1, ); - let res = mem_write_gp_log_and_fill(3, address, state, &mut row, state.registers.stack_top); + let res = mem_write_gp_log_and_fill(2, address, state, &mut row, state.registers.stack_top); Some(res) }; push_no_write( @@ -380,7 +380,10 @@ pub(crate) fn generate_set_context( let old_sp_addr = MemoryAddress::new(old_ctx, Segment::ContextMetadata, sp_field); let new_sp_addr = MemoryAddress::new(new_ctx, Segment::ContextMetadata, sp_field); - let log_write_old_sp = mem_write_gp_log_and_fill(1, old_sp_addr, state, &mut row, sp_to_save); + // This channel will hold in limb 0 and 1 the one-limb value of two separate memory operations: + // the old stack pointer write and the new stack pointer read. + // Channels only matter for time stamps: the write must happen before the read. + let log_write_old_sp = mem_write_log(GeneralPurpose(1), old_sp_addr, state, sp_to_save); let (new_sp, log_read_new_sp) = if old_ctx == new_ctx { let op = MemoryOp::new( MemoryChannel::GeneralPurpose(2), @@ -389,23 +392,9 @@ pub(crate) fn generate_set_context( MemoryOpKind::Read, sp_to_save, ); - - let channel = &mut row.mem_channels[2]; - assert_eq!(channel.used, F::ZERO); - channel.used = F::ONE; - channel.is_read = F::ONE; - channel.addr_context = F::from_canonical_usize(new_ctx); - channel.addr_segment = F::from_canonical_usize(Segment::ContextMetadata.unscale()); - channel.addr_virtual = F::from_canonical_usize(new_sp_addr.virt); - let val_limbs: [u64; 4] = sp_to_save.0; - for (i, limb) in val_limbs.into_iter().enumerate() { - channel.value[2 * i] = F::from_canonical_u32(limb as u32); - channel.value[2 * i + 1] = F::from_canonical_u32((limb >> 32) as u32); - } - (sp_to_save, op) } else { - mem_read_gp_with_log_and_fill(2, new_sp_addr, state, &mut row) + mem_read_with_log(GeneralPurpose(2), new_sp_addr, state) }; // If the new stack isn't empty, read stack_top from memory. @@ -425,7 +414,7 @@ pub(crate) fn generate_set_context( let new_top_addr = MemoryAddress::new(new_ctx, Segment::Stack, new_sp - 1); let (new_top, log_read_new_top) = - mem_read_gp_with_log_and_fill(3, new_top_addr, state, &mut row); + mem_read_gp_with_log_and_fill(2, new_top_addr, state, &mut row); state.registers.stack_top = new_top; state.traces.push_memory(log_read_new_top); } else { @@ -705,27 +694,30 @@ pub(crate) fn generate_syscall( let handler_addr_addr = handler_jumptable_addr + (opcode as usize) * (BYTES_PER_OFFSET as usize); assert_eq!(BYTES_PER_OFFSET, 3, "Code below assumes 3 bytes per offset"); - let (handler_addr0, log_in0) = mem_read_gp_with_log_and_fill( - 1, - MemoryAddress::new(0, Segment::Code, handler_addr_addr), - state, - &mut row, - ); - let (handler_addr1, log_in1) = mem_read_gp_with_log_and_fill( - 2, - MemoryAddress::new(0, Segment::Code, handler_addr_addr + 1), - state, - &mut row, - ); - let (handler_addr2, log_in2) = mem_read_gp_with_log_and_fill( - 3, - MemoryAddress::new(0, Segment::Code, handler_addr_addr + 2), - state, - &mut row, - ); + let base_address = MemoryAddress::new(0, Segment::Code, handler_addr_addr); + let bytes = (0..BYTES_PER_OFFSET as usize) + .map(|i| { + let address = MemoryAddress { + virt: base_address.virt + i, + ..base_address + }; + let val = state.memory.get(address); + val.low_u32() as u8 + }) + .collect_vec(); + + let packed_int = U256::from_big_endian(&bytes); + + let jumptable_channel = &mut row.mem_channels[1]; + jumptable_channel.is_read = F::ONE; + jumptable_channel.addr_context = F::ZERO; + jumptable_channel.addr_segment = F::from_canonical_usize(Segment::Code as usize); + jumptable_channel.addr_virtual = F::from_canonical_usize(handler_addr_addr); + jumptable_channel.value[0] = F::from_canonical_usize(u256_to_usize(packed_int)?); + + byte_packing_log(state, base_address, bytes); - let handler_addr = (handler_addr0 << 16) + (handler_addr1 << 8) + handler_addr2; - let new_program_counter = u256_to_usize(handler_addr)?; + let new_program_counter = u256_to_usize(packed_int)?; let gas = U256::from(state.registers.gas_used); @@ -734,14 +726,15 @@ pub(crate) fn generate_syscall( + (gas << 192); // `ArithmeticStark` range checks `mem_channels[0]`, which contains - // the top of the stack, `mem_channels[1]`, `mem_channels[2]` and - // next_row's `mem_channels[0]` which contains the next top of the stack. + // the top of the stack, `mem_channels[1]`, which contains the new PC, + // `mem_channels[2]`, which is empty, and next_row's `mem_channels[0]`, + // which contains the next top of the stack. // Our goal here is to range-check the gas, contained in syscall_info, // stored in the next stack top. let range_check_op = arithmetic::Operation::range_check( state.registers.stack_top, - handler_addr0, - handler_addr1, + packed_int, + U256::from(0), U256::from(opcode), syscall_info, ); @@ -757,9 +750,6 @@ pub(crate) fn generate_syscall( log::debug!("Syscall to {}", KERNEL.offset_name(new_program_counter)); state.traces.push_arithmetic(range_check_op); - state.traces.push_memory(log_in0); - state.traces.push_memory(log_in1); - state.traces.push_memory(log_in2); state.traces.push_cpu(row); Ok(()) @@ -950,27 +940,29 @@ pub(crate) fn generate_exception( let handler_addr_addr = handler_jumptable_addr + (exc_code as usize) * (BYTES_PER_OFFSET as usize); assert_eq!(BYTES_PER_OFFSET, 3, "Code below assumes 3 bytes per offset"); - let (handler_addr0, log_in0) = mem_read_gp_with_log_and_fill( - 1, - MemoryAddress::new(0, Segment::Code, handler_addr_addr), - state, - &mut row, - ); - let (handler_addr1, log_in1) = mem_read_gp_with_log_and_fill( - 2, - MemoryAddress::new(0, Segment::Code, handler_addr_addr + 1), - state, - &mut row, - ); - let (handler_addr2, log_in2) = mem_read_gp_with_log_and_fill( - 3, - MemoryAddress::new(0, Segment::Code, handler_addr_addr + 2), - state, - &mut row, - ); + let base_address = MemoryAddress::new(0, Segment::Code, handler_addr_addr); + let bytes = (0..BYTES_PER_OFFSET as usize) + .map(|i| { + let address = MemoryAddress { + virt: base_address.virt + i, + ..base_address + }; + let val = state.memory.get(address); + val.low_u32() as u8 + }) + .collect_vec(); + + let packed_int = U256::from_big_endian(&bytes); + + let jumptable_channel = &mut row.mem_channels[1]; + jumptable_channel.is_read = F::ONE; + jumptable_channel.addr_context = F::ZERO; + jumptable_channel.addr_segment = F::from_canonical_usize(Segment::Code as usize); + jumptable_channel.addr_virtual = F::from_canonical_usize(handler_addr_addr); + jumptable_channel.value[0] = F::from_canonical_usize(u256_to_usize(packed_int)?); - let handler_addr = (handler_addr0 << 16) + (handler_addr1 << 8) + handler_addr2; - let new_program_counter = u256_to_usize(handler_addr)?; + byte_packing_log(state, base_address, bytes); + let new_program_counter = u256_to_usize(packed_int)?; let gas = U256::from(state.registers.gas_used); @@ -982,14 +974,15 @@ pub(crate) fn generate_exception( let opcode = state.memory.get(address); // `ArithmeticStark` range checks `mem_channels[0]`, which contains - // the top of the stack, `mem_channels[1]`, `mem_channels[2]` and - // next_row's `mem_channels[0]` which contains the next top of the stack. + // the top of the stack, `mem_channels[1]`, which contains the new PC, + // `mem_channels[2]`, which is empty, and next_row's `mem_channels[0]`, + // which contains the next top of the stack. // Our goal here is to range-check the gas, contained in syscall_info, // stored in the next stack top. let range_check_op = arithmetic::Operation::range_check( state.registers.stack_top, - handler_addr0, - handler_addr1, + packed_int, + U256::from(0), opcode, exc_info, ); @@ -1004,9 +997,6 @@ pub(crate) fn generate_exception( log::debug!("Exception to {}", KERNEL.offset_name(new_program_counter)); state.traces.push_arithmetic(range_check_op); - state.traces.push_memory(log_in0); - state.traces.push_memory(log_in1); - state.traces.push_memory(log_in2); state.traces.push_cpu(row); Ok(())