Skip to main content

Operand Stack

Miden VM is a stack machine. The stack is a push-down stack of practically unlimited depth (in practical terms, the depth will never exceed 2322^{32}), but only the top 1616 items are directly accessible to the VM. Items on the stack are elements in a prime field with modulus 264232+12^{64} - 2^{32} + 1.

To keep the constraint system for the stack manageable, we impose the following rules:

  1. All operations executed on the VM can shift the stack by at most one item. That is, the end result of an operation must be that the stack shrinks by one item, grows by one item, or the number of items on the stack stays the same.
  2. Stack depth must always be greater than or equal to 1616. At the start of program execution, the stack is initialized with exactly 1616 input values, all of which could be 00's.
  3. By the end of program execution, exactly 1616 items must remain on the stack (again, all of them could be 00's). These items comprise the output of the program.

To ensure that managing stack depth does not impose significant burden, we adopt the following rule:

  • When the stack depth is 1616, removing additional items from the stack does not change its depth. To keep the depth at 1616, 00's are inserted into the deep end of the stack for each removed item.

Stack representation

The VM allocates 1919 trace columns for the stack. The layout of the columns is illustrated below.

The meaning of the above columns is as follows:

  • s0...s15s_0 ... s_{15} are the columns representing the top 1616 slots of the stack.
  • Column b0b_0 contains the number of items on the stack (i.e., the stack depth). In the above picture, there are 16 items on the stacks, so b0=16b_0 = 16.
  • Column b1b_1 contains an address of a row in the "overflow table" in which we'll store the data that doesn't fit into the top 1616 slots. When b1=0b_1 = 0, it means that all stack data fits into the top 1616 slots of the stack.
  • Helper column h0h_0 is used to ensure that stack depth does not drop below 1616. Values in this column are set by the prover non-deterministically to 1b016\frac{1}{b_0 - 16} when b016b_0 \neq 16, and to any other value otherwise.

Overflow table

To keep track of the data which doesn't fit into the top 1616 stack slots, we'll use an overflow table. This will be a virtual table. To represent this table, we'll use a single auxiliary column p1p_1.

The table itself can be thought of as having 3 columns as illustrated below.

The meaning of the columns is as follows:

  • Column t0t_0 contains row address. Every address in the table must be unique.
  • Column t1t_1 contains the value that overflowed the stack.
  • Column t2t_2 contains the address of the row containing the value that overflowed the stack right before the value in the current row. For example, in the picture above, first value aa overflowed the stack, then bb overflowed the stack, and then value cc overflowed the stack. Thus, row with value bb points back to the row with value aa, and row with value cc points back to the row with value bb.

To reduce a table row to a single value, we'll compute a randomized product of column values as follows:

ri=α0+α1t0,i+α2t1,i+α3t2,ir_i = \alpha_0 + \alpha_1 \cdot t_{0, i} + \alpha_2 \cdot t_{1, i} + \alpha_3 \cdot t_{2, i}

Then, when row ii is added to the table, we'll update the value in the p1p_1 column like so:

p1=p1rip_1' = p_1 \cdot r_i

Analogously, when row ii is removed from the table, we'll update the value in column p1p_1 like so:

p1=p1rip_1' = \frac{p_1}{r_i}

The initial value of p1p_1 is set to 11. Thus, if by the time Miden VM finishes executing a program the table is empty (we added and then removed exactly the same set of rows), p1p_1 will also be equal to 11.

There are a couple of other rules we'll need to enforce:

  • We can delete a row only after the row has been inserted into the table.
  • We can't insert a row with the same address twice into the table (even if the row was inserted and then deleted).

How these are enforced will be described a bit later.

Right shift

If an operation adds data to the stack, we say that the operation caused a right shift. For example, PUSH and DUP operations cause a right shift. Graphically, this looks like so:

Here, we pushed value v17v_{17} onto the stack. All other values on the stack are shifted by one slot to the right and the stack depth increases by 11. There is not enough space at the top of the stack for all 1717 values, thus, v1v_1 needs to be moved to the overflow table.

To do this, we need to rely on another column: k0k_0. This is a system column which keeps track of the current VM cycle. The value in this column is simply incremented by 11 with every step.

The row we want to add to the overflow table is defined by tuple (clk,v1,0)(clk, v1, 0), and after it is added, the table would look like so:

The reason we use VM clock cycle as row address is that the clock cycle is guaranteed to be unique, and thus, the same row can not be added to the table twice.

Let's push another item onto the stack:

Again, as we push v18v_{18} onto the stack, all items on the stack are shifted to the right, and now v2v_2 needs to be moved to the overflow table. The tuple we want to insert into the table now is (clk+1,v2,clk)(clk+1, v2, clk). After the operation, the overflow table will look like so:

Notice that t2t_2 for row which contains value v2v_2 points back to the row with address clkclk.

Overall, during a right shift we do the following:

  • Increment stack depth by 11.
  • Shift stack columns s0,...,s14s_0, ..., s_{14} right by 11 slot.
  • Add a row to the overflow table described by tuple (k0,s15,b0)(k_0, s_{15}, b_0).
  • Set the next value of b1b_1 to the current value of k0k_0.

Also, as mentioned previously, the prover sets values in h0h_0 non-deterministically to 1b016\frac{1}{b_0 - 16}.

Left shift

If an operation removes an item from the stack, we say that the operation caused a left shift. For example, a DROP operation causes a left shift. Assuming the stack is in the state we left it at the end of the previous section, graphically, this looks like so:

Overall, the during the left shift we do the following:

  • When stack depth is greater than 1616:
    • Decrement stack depth by 11.
    • Shift stack columns s1,...,s15s_1, ..., s_{15} left by 11 slot.
    • Remove a row from the overflow table with t0t_0 equal to the current value of b1b_1.
    • Set the next value of s15s_{15} to the value in t1t_1 of the removed overflow table row.
    • Set the next value of b1b_1 to the value in t2t_2 of the removed overflow table row.
  • When the stack depth is equal to 1616:
    • Keep the stack depth the same.
    • Shift stack columns s1,...,s15s_1, ..., s_{15} left by 11 slot.
    • Set the value of s15s_{15} to 00.
    • Set the value to h0h_0 to 00 (or any other value).

If the stack depth becomes (or remains) 1616, the prover can set h0h_0 to any value (e.g., 00). But if the depth is greater than 1616 the prover sets h0h_0 to 1b016\frac{1}{b_0 - 16}.

AIR Constraints

To simplify constraint descriptions, we'll assume that the VM exposes two binary flag values described below.

FlagDegreeDescription
fshrf_{shr}6When this flag is set to 11, the instruction executing on the VM is performing a "right shift".
fshlf_{shl}5When this flag is set to 11, the instruction executing on the VM is performing a "left shift".

These flags are mutually exclusive. That is, if fshl=1f_{shl}=1, then fshr=0f_{shr}=0 and vice versa. However, both flags can be set to 00 simultaneously. This happens when the executed instruction does not shift the stack. How these flags are computed is described here.

Stack overflow flag

Additionally, we'll define a flag to indicate whether the overflow table contains values. This flag will be set to 00 when the overflow table is empty, and to 11 otherwise (i.e., when stack depth >16>16). This flag can be computed as follows:

fov=(b016)h0 | degree=2f_{ov} = (b_0 - 16) \cdot h_0 \text{ | degree} = 2

To ensure that this flag is set correctly, we need to impose the following constraint:

(1fov)(b016)=0 | degree=3(1 - f_{ov}) \cdot (b_0 - 16) = 0 \text{ | degree} = 3

The above constraint can be satisfied only when either of the following holds:

  • b0=16b_0 = 16, in which case fovf_{ov} evaluates to 00, regardless of the value of h0h_0.
  • fov=1f_{ov} = 1, in which case b0b_0 cannot be equal to 1616 (and h0h_0 must be set to 1b016\frac{1}{b_0 - 16}).

Stack depth constraints

To make sure stack depth column b0b_0 is updated correctly, we need to impose the following constraints:

ConditionConstraint__Description
fshr=1f_{shr}=1b0=b0+1b'_0 = b_0 + 1When the stack is shifted to the right, stack depth should be incremented by 11.
fshl=1f_{shl}=1
fov=1f_{ov}=1
b0=b01b'_0 = b_0 - 1When the stack is shifted to the left and the overflow table is not empty, stack depth should be decremented by 11.
otherwiseb0=b0b'_0 = b_0In all other cases, stack depth should not change.

We can combine the above constraints into a single expression as follows:

b0b0+fshlfovfshr=0 | degree=7b'_0 - b_0 + f_{shl} \cdot f_{ov} - f_{shr} = 0 \text{ | degree} = 7

Overflow table constraints

When the stack is shifted to the right, a tuple (k0,s15,b1)(k_0, s_{15}, b_1) should be added to the overflow table. We will denote value of the row to be added to the table as follows:

v=α0+α1k0+α2s15+α3b1v = \alpha_0 + \alpha_1 \cdot k_0 + \alpha_2 \cdot s_{15} + \alpha_3 \cdot b_1

When the stack is shifted to the left, a tuple (b1,s15,b1)(b_1, s'_{15}, b'_1) should be removed from the overflow table. We will denote value of the row to be removed from the table as follows.

u=α0+α1b1+α2s15+α3b1u = \alpha_0 + \alpha_1 \cdot b_1 + \alpha_2 \cdot s'_{15} + \alpha_3 \cdot b'_1

Using the above variables, we can ensure that right and left shifts update the overflow table correctly by enforcing the following constraint:

p1(ufshlfov+1fshlfov)=p1(vfshr+1fshr) | degree=9p_1' \cdot (u \cdot f_{shl} \cdot f_{ov} + 1 - f_{shl} \cdot f_{ov}) = p_1 \cdot (v \cdot f_{shr} + 1 - f_{shr}) \text{ | degree} = 9

The above constraint reduces to the following under various flag conditions:

ConditionApplied constraint
fshl=1f_{shl}=1, fshr=0f_{shr}=0, fov=0f_{ov}=0p1=p1p_1' = p_1
fshl=1f_{shl}=1, fshr=0f_{shr}=0, fov=1f_{ov}=1p1u=p1p_1' \cdot u = p_1
fshl=0f_{shl}=0, fshr=1f_{shr}=1, fov=1 or 0f_{ov}=1 \text{ or } 0p1=p1vp_1' = p_1 \cdot v
fshl=0f_{shl}=0, fshr=0f_{shr}=0, fov=1 or 0f_{ov}=1 \text{ or } 0p1=p1p_1' = p_1

Notice that in the case of the left shift, the constraint forces the prover to set the next values of s15s_{15} and b1b_1 to values t1t_1 and t2t_2 of the row removed from the overflow table.

In case of a right shift, we also need to make sure that the next value of b1b_1 is set to the current value of k0k_0. This can be done with the following constraint:

fshr(b1k0)=0 | degree=7f_{shr} \cdot (b'_1 - k_0) = 0 \text{ | degree} = 7

In case of a left shift, when the overflow table is empty, we need to make sure that a 00 is "shifted in" from the right (i.e., s15s_{15} is set to 00). This can be done with the following constraint:

fshl(1fov)s15=0 | degree=8f_{shl} \cdot (1 - f_{ov}) \cdot s_{15}' = 0 \text{ | degree} = 8

Boundary constraints

In addition to the constraints described above, we also need to enforce the following boundary constraints:

  • b0=16b_0 = 16 at the first and at the last row of execution trace.
  • b1=0b_1 = 0 at the first and at the last row of execution trace.
  • p1=1p_1 = 1 at the first and at the last row of execution trace.