Stack handling
Top of the stack
The majority of memory operations involve the stack. The stack is a
segment in memory, and stack operations (popping or pushing) use the
memory channels. Every CPU instruction performs between 0 and 3 pops,
and may push at most once. However, for efficiency purposes, we hold the
top of the stack in the first memory channel
current_row.mem_channels[0]
, only writing it in memory if necessary.
Top reading and writing
When a CPU instruction modifies the stack, it must update the top of the stack accordingly. There are three cases.
-
The instruction pops and pushes: The new top of the stack is stored in
next_row.mem_channels[0]
; it may be computed by the instruction, or it could be read from memory. In either case, the instruction is responsible for settingnext_row.mem_channels[0]
's flags and address columns correctly. After use, the previous top of the stack is discarded and doesn't need to be written in memory. -
The instruction pushes, but doesn't pop: The new top of the stack is stored in
next_row.mem_channels[0]
; it may be computed by the instruction, or it could be read from memory. In either case, the instruction is responsible for settingnext_row.mem_channels[0]
's flags and address columns correctly. If the stack wasn't empty (current_row.stack_len > 0
), the instruction performs a memory read incurrent_row.partial_ channel
.current_row.partial_channel
shares its values withcurrent_ row.mem_channels[0]
(which holds the current top of the stack). If the stack was empty,current_row.partial_channel
is disabled. -
The instruction pops, but doesn't push: After use, the current top of the stack is discarded and doesn't need to be written in memory. If the stack isn't empty now (
current_row.stack_len > num_pops
), the new top of the stack is set innext_row.mem_channels[0]
with a memory read from the stack segment. If the stack is now empty,next_row.mem_channels[0]
is disabled.
In the last two cases, there is an edge case if current_row.stack_len
is equal to a special_len
. For a strictly pushing instruction, this
happens if the stack is empty, and special_len = 0
. For a strictly
popping instruction, this happens if the next stack is empty, i.e. if
all remaining elements are popped, and special_len = num_pops
. Note
that we do not need to check for values below num_pops
, since this
would be a stack underflow exception which is handled separately. The
edge case is detected with the compound flag
where
and stack_inv_aux
is constrained to be the modular inverse of
not_special_ len
if it's non-zero, or 0 otherwise. The flag is 1 if
stack_len
is equal to special_len
, and 0 otherwise.
This logic can be found in code in the eval_packed_one
function of
stack.rs.
The function multiplies all of the stack constraints with the degree 1
filter associated with the current instruction.
Operation flag merging
To reduce the total number of columns, many operation flags are merged
together (e.g. DUP
and SWAP
) and are distinguished with the binary
decomposition of their opcodes. The filter for a merged operation is now
of degree 2: for example, is_swap = dup_swap * opcode_bits[4]
since
the 4th bit is set to 1 for a SWAP
and 0 for a DUP
. If the two
instructions have different stack behaviors, this can be a problem:
eval_packed_one
's constraints are already of degree 3 and it can't
support degree 2 filters.
When this happens, stack constraints are defined manually in the
operation's dedicated file (e.g. dup_swap.rs
). Implementation details
vary case-by-case and can be found in the files.
Stack length checking
The CPU must make sure that the stack length never goes below zero and, in user mode, never grows beyond the maximum stack size. When this happens, an honest prover should trigger the corresponding exception. If a malicious prover doesn't trigger the exception, constraints must fail the proof.
Stack underflow
There is no explicit constraint checking for stack underflow. An
underflow happens when the CPU tries to pop the empty stack, which would
perform a memory read at virtual address -1
. Such a read cannot
succeed: in Memory, the range-check argument requires the gap between
two consecutive addresses to be lower than the length of the Memory
trace. Since the prime of the Plonky2 field is 64-bit long, this would
require a Memory trace longer than .
Stack overflow
An instruction can only push at most once, meaning that an overflow
occurs whenever the stack length is exactly one more than the maximum
stack size () in user mode. To constrain this, the column
stack_len_bounds_aux
contains:
-
the modular inverse of
stack_len - 1025
if we're in user mode andstack_len
1025
, -
0 if
stack_len = 1025
or if we're in kernel mode.
Then overflow can be checked with the flag
The flag is 1 if stack_len = 1025
and we're in user mode, and 0
otherwise.
Because stack_len_bounds_aux
is a shared general column, we only check
this constraint after an instruction that can actually trigger an
overflow, i.e. a pushing, non-popping instruction.