Skip to main content
Version: Next (unstable)

Miden VM decoder AIR constraints

In this section we describe AIR constraints for Miden VM program decoder. These constraints enforce that the execution trace generated by the prover when executing a particular program complies with the rules described in the previous section.

To refer to decoder execution trace columns, we use the following names (these are the same names as in the previous section). Additionally, we denote the register containing the value at the top of the stack as s0s_0.

ColumnsRegisters
00aa
11--77b0,…,b6b_0, \ldots, b_6
88--1515h0,…,h7h_0, \ldots, h_7
1616spsp
1717gcgc
1818oxox
1919full_batch
2020batch_size_code
2121--2222e0,e1e_0, e_1

We assume that the VM exposes a flag per operation which is set to 11 when the operation is executed, and to 00 otherwise. The notation for such flags is fopnamef_{opname}. For example, when the VM executes a PUSH operation, flag fpush=1f_{push} = 1. All flags are mutually exclusive - i.e., when one flag is set to 11 all other flags are set to 00. The flags are computed based on values in op_bits columns.

AIR constraints for the decoder involve operations listed in the table below. For each operation we also provide the degree of the corresponding flag and the effect that the operation has on the operand stack (however, in this section we do not cover the constraints needed to enforce the correct transition of the operand stack).

OperationFlagDegreeEffect on stack
JOINfjoinf_{join}5Stack remains unchanged.
SPLITfsplitf_{split}5Top stack element is dropped.
LOOPfloopf_{loop}5Stack remains unchanged.
REPEATfrepeatf_{repeat}4Top stack element is dropped.
SPANfspanf_{span}5Stack remains unchanged.
RESPANfrespanf_{respan}4Stack remains unchanged.
DYNfdynf_{dyn}5Top stack element is dropped.
DYNCALLfdyncallf_{dyncall}5Top stack element is dropped.
CALLfcallf_{call}4Stack remains unchanged.
SYSCALLfsyscallf_{syscall}4Stack remains unchanged.
ENDfendf_{end}4When exiting a loop block, top stack element is dropped; otherwise, the stack remains unchanged.
HALTfhaltf_{halt}4Stack remains unchanged.
PUSHfpushf_{push}5An immediate value is pushed onto the stack.
EMITfemitf_{emit}7Stack remains unchanged.

We also use the control flow flag fctrlf_{ctrl} exposed by the VM, which is set to 11 for the control-flow operations SPAN, JOIN, SPLIT, LOOP, END, REPEAT, RESPAN, HALT, DYN, DYNCALL, CALL, and SYSCALL (and 00 otherwise). It has degree 55.

As described previously, the prover supplies parts of the program by populating trace cells non-deterministically. Typed LogUp messages bind those values to the block hash, block stack, and op-group virtual tables. Local transition constraints select the required insertions and removals, while lookup closure and the program-hash boundary contribution bind the execution trace to the claimed program.

In the sections below, we describe constraints according to their logical grouping. However, we start out with a set of general constraints which are applicable to multiple parts of the decoder.

General constraints​

When SPLIT operation is executed, the top of the operand stack must contain a binary value:

fsplit⋅(s02−s0)=0 | degree=7f_{split} \cdot (s_0^2 - s_0) = 0 \text{ | degree} = 7

When a DYN operation is executed, the second half of the hasher registers (h4,…,h7h_4,\dots,h_7) must be set to 00 (the first half holds the callee digest):

fdyn⋅hi=0 for i∈[4,8) | degree=6f_{dyn} \cdot h_i = 0 \text { for } i \in [4, 8) \text{ | degree} = 6

A REPEAT operation requires 11 at the top of the operand stack:

frepeat⋅(1−s0)=0 | degree=5f_{repeat} \cdot (1 - s_0) = 0 \text{ | degree} = 5

On each transition, REPEAT must follow an END whose h4h_4 marks the completed node as a loop body. The block-hash relation authenticates this flag on the END row:

frepeat′⋅(1−fend⋅h4)=0 | degree=9f_{repeat}' \cdot (1 - f_{end} \cdot h_4) = 0 \text{ | degree} = 9

LOOP cannot transition directly to END; that would skip the body if its body-hash multiplicity were zero:

floop⋅fend′=0 | degree=9f_{loop} \cdot f_{end}' = 0 \text{ | degree} = 9

The first row has no predecessor, so it cannot be a REPEAT operation:

frepeat=0 on the first rowf_{repeat} = 0 \text{ on the first row}

When RESPAN operation is executed, we need to make sure that the block ID is incremented by 11:

frespan⋅(a′−a−1)=0 | degree=5f_{respan} \cdot (a' - a - 1) = 0 \text{ | degree} = 5

When END operation is executed and we are exiting a loop block (i.e., is_loop, value which is stored in h5h_5, is 11), the value at the top of the operand stack must be 00:

fend⋅h5⋅s0=0 | degree=6f_{end} \cdot h_5 \cdot s_0 = 0 \text{ | degree} = 6

A HALT instruction can be followed only by another HALT instruction:

fhalt⋅(1−fhalt′)=0 | degree=8f_{halt} \cdot (1 - f_{halt}') = 0 \text{ | degree} = 8

When a HALT operation is executed, block address column must be 00:

fhalt⋅a=0 | degree=5f_{halt} \cdot a = 0 \text{ | degree} = 5

Values in op_bits columns must be binary (i.e., either 11 or 00):

bi2−bi=0 for i∈[0,7) | degree=2b_i^2 - b_i = 0 \text{ for } i \in [0, 7) \text{ | degree} = 2

We also use two extra columns (e0e_0, e1e_1) for degree reduction in the operation flag computation:

e0−b6⋅(1−b5)⋅b4=0 | degree=3e_0 - b_6 \cdot (1 - b_5) \cdot b_4 = 0 \text{ | degree} = 3
e1−b6⋅b5=0 | degree=2e_1 - b_6 \cdot b_5 = 0 \text{ | degree} = 2

Since e0e_0 selects prefix 101, b6−e0b_6 - e_0 selects prefixes 100 and 11. The two terms below select disjoint opcode groups: the first forces b0=0b_0 = 0 for prefixes 100 and 11, and the second rejects opcode 9595 under prefix 101:

(b6−e0+e0⋅b3⋅b2⋅b1)⋅b0=0 | degree=5(b_6 - e_0 + e_0 \cdot b_3 \cdot b_2 \cdot b_1) \cdot b_0 = 0 \text{ | degree} = 5

Prefix 11 also requires b1=0b_1 = 0:

e1⋅b1=0 | degree=2e_1 \cdot b_1 = 0 \text{ | degree} = 2

When the value in in_span column is set to 11, control flow operations cannot be executed on the VM, but when in_span flag is 00, only control flow operations can be executed on the VM:

1−sp−fctrl=0 | degree=51 - sp - f_{ctrl} = 0 \text{ | degree} = 5

Block hash computation constraints​

As described previously, when the VM starts executing a new block, it also initiates computation of the block's hash. There are two separate methodologies for computing block hashes.

For join and split blocks, the hash is computed directly from the hashes of the block's children. The prover provides these child hashes non-deterministically by populating registers h0,...,h7h_0,..., h_7. For loop blocks, only the loop body hash is provided in h0..h3h_0..h_3 and the remaining registers h4..h7h_4..h_7 are set to 00 (padding to a full 8-element block). For dyn, the first half holds the callee digest read from memory and the second half of the hasher registers (h4,…,h7h_4,\dots,h_7) is constrained to 00; the digest itself may be the zero word. The callee digest participates in the memory and block-hash messages, while the domain-separated DYN hash request uses an all-zero input block. The hasher is initialized using the hash controller, and we use the controller-row address as the block's ID. A controller row overlays both the compression input and its output chaining value; for these framed block-hash operations, that value is the block digest read by END at the same address.

For basic blocks, the hash is computed by absorbing a linear sequence of instructions (organized into operation groups and batches) into the hasher and then returning the result. The prover provides operation batches non-deterministically by populating registers h0,...,h7h_0, ..., h_7. Similarly to other blocks, the hasher is initialized using the hash controller at the start of the block, and we use the address of the first controller row as the ID of the first operation batch in the block. As we absorb additional operation batches into the hasher (by executing RESPAN), the next batch uses the next controller row, so the batch address is incremented by 11. We read the result from that final controller row when the END operation is executed for the block.

Chiplets bus constraints​

The decoder communicates with the hash, memory, and kernel-ROM chiplets through typed LogUp messages. Each decoder request has multiplicity −1-1; the owning chiplet supplies the matching message with positive multiplicity. The shared LogUp recurrence and cross-AIR closure are described in the linked overview.

In constructing values for decoder bus requests, we use the typed hasher message format described in the hasher chiplet. Let PkP_k be the domain-separated prefix for message kind kk. A message is reduced as

Hk(addr,node,payload)=Pk+addr+β⋅node+∑i=0∣payload∣−1βi+2⋅payloadi.H_k(addr, node, payload) = P_k + addr + \beta \cdot node + \sum_{i=0}^{|payload|-1} \beta^{i+2} \cdot payload_i.

The message kind selects a distinct prefix for a full-state initialization, next-block absorption, or four-Felt result/CV return. Decoder block-hash messages use node index zero. Let d=∑i=06(bi⋅2i)d = \sum_{i=0}^6(b_i \cdot 2^i) denote the opcode value. Let CV(d)CV(d) denote the Eidos two-to-one chaining word derived from that opcode, and let CVspan(gc)CV_{span}(gc) denote the basic-block chaining word derived from its logical operation-group count.

To simplify constraint description, we define the following values:

hctrl=Hinit(a′,0,[h0,…,h7,CV(d)])h_{ctrl} = H_{init}(a', 0, [h_0,\ldots,h_7,CV(d)]) hspan=Hinit(a′,0,[h0,…,h7,CVspan(gc)])h_{span} = H_{init}(a', 0, [h_0,\ldots,h_7,CV_{span}(gc)]) hrespan=Habsorb(a′,0,[h0,…,h7])h_{respan} = H_{absorb}(a', 0, [h_0,\ldots,h_7]) hend=Hreturn(a,0,[h0,…,h3])h_{end} = H_{return}(a, 0, [h_0,\ldots,h_3]) hdyn=Hinit(a′,0,[0,…,0,CV(d)])h_{dyn} = H_{init}(a', 0, [0,\ldots,0,CV(d)])

For the other typed chiplet messages below, write

Kcall(q)=Pkernel_call+∑i=03βiqiK_{call}(q) = P_{kernel\_call} + \sum_{i=0}^{3}\beta^i q_i

and

Mk(ctx,addr,clk,p)=Pk+ctx+βaddr+β2clk+∑i=0∣p∣−1βi+3pi.M_k(ctx, addr, clk, p) = P_k + ctx + \beta addr + \beta^2 clk + \sum_{i=0}^{|p|-1}\beta^{i+3}p_i. fctrli=fjoin+fsplit+floop | degree=5f_{ctrli} = f_{join} + f_{split} + f_{loop} \text{ | degree} = 5

In the above, fctrlif_{ctrli} is set to 11 when a control flow operation that signifies the initialization of a control block is being executed on the VM (only those control blocks that don't do any concurrent requests to the chiplets bus). Otherwise, it is set to 00. An exception is made for the DYN, DYNCALL, CALL and SYSCALL operations, since although they initialize a control block, they also run another concurrent bus request, and so are handled separately.

When a control block initializer (JOIN, SPLIT, or LOOP) is executed, the decoder removes hctrlh_{ctrl}. The opcode-derived Eidos chaining word domain-separates the new hash.

SYSCALL removes both hctrlh_{ctrl} and the following kernel-ROM request:

kproc=Kcall([h0,…,h3])k_{proc} = K_{call}([h_0,\ldots,h_3])

The values h0,h1,h2,h3h_0, h_1, h_2, h_3 contain the root hash of the procedure being called.

Similar to SYSCALL, CALL is handled separately, since in addition to communicating with the hash chiplet, it must also initialize the frame memory pointer (stored in memory at constant address fmpaddr with constant value fmpinit):

mfmpwrite=Mwrite_element(ctx′,fmpaddr,clk,[fmpinit])m_{fmpwrite} = M_{write\_element}(ctx', fmpaddr, clk, [fmpinit])

In the above, mfmpwritem_{fmpwrite} represents a "write element" memory request equivalent to mem[fmpaddr] = fmpinit (in pseudo-code) in the new memory context (i.e. the memory context of the callee). The VM uses fmpaddr=232−2fmpaddr = 2^{32} - 2 and fmpinit=231fmpinit = 2^{31}.

CALL removes hctrlh_{ctrl} and mfmpwritem_{fmpwrite} as two separate interactions.

Similar to SYSCALL and CALL, DYN and DYNCALL are handled separately, since in addition to communicating with the hash chiplet they must also issue a memory read operation for the hash of the procedure being called.

mdynordyncall=Mread_word(ctx,s0,clk,[h0,…,h3])m_{dynordyncall} = M_{read\_word}(ctx, s_0, clk, [h_0,\ldots,h_3])

DYN removes hdynh_{dyn} and mdynordyncallm_{dynordyncall}; DYNCALL additionally removes mfmpwritem_{fmpwrite}. The hash request carries an all-zero block and the opcode-derived chaining word, while mdynordyncallm_{dynordyncall} binds the callee digest in h0,…,h3h_0,\ldots,h_3 to the memory word at s0s_0. mdynordyncallm_{dynordyncall} represents a memory word read request from address s0s_0, where the result is placed in the first half of the decoder hasher trace. Note that similar to CALL, DYNCALL also creates a new memory context, and hence must also initialize the fmp.

When SPAN is executed, a new hasher is initialized and the contents of h0,...,h7h_0, ..., h_7 are absorbed into it. The input CV is CVspan(gc)CV_{span}(gc), which binds the logical operation-group count. The decoder removes hspanh_{span} on that row.

When RESPAN is executed, the contents of h0,...,h7h_0, ..., h_7 (which contain the new operation batch) are absorbed into the hasher. The decoder removes hrespanh_{respan} on that row.

When END is executed, the hash result is copied into registers h0,..,h3h_0, .., h_3. The decoder removes hendh_{end} on that row. Together, these operation-gated negative contributions require the hasher, memory, and kernel-ROM traces to provide every requested typed message.

Block stack table constraints​

As described previously, block stack table keeps track of program blocks currently executing on the VM. Thus, whenever the VM starts executing a new block, an entry for this block is added to the block stack table. And when execution of a block completes, it is removed from the block stack table.

The BlockStackTable message has two tagged variants. A continuation is Sc(block,parent,is_loop)S_c(block,parent,is\_loop); a caller frame is Sf(block,parent,ctx,b0,b1,fn_hash)S_f(block,parent,ctx,b_0,b_1,fn\_hash). The caller-frame tag prevents its encoding from colliding with a continuation whose saved payload is zero. Operation flags gate these signed interactions:

  • JOIN, SPLIT, SPAN, and DYN add Sc(a′,a,0)S_c(a',a,0).
  • LOOP adds Sc(a′,a,1)S_c(a',a,1). Its do-while semantics make is_loop unconditionally one.
  • CALL and SYSCALL add Sf(a′,a,ctx,b0,b1,fn_hash)S_f(a',a,ctx,b_0,b_1,fn\_hash), saving the caller state.
  • DYNCALL adds Sf(a′,a,ctx,h4,h5,fn_hash)S_f(a',a,ctx,h_4,h_5,fn\_hash); h4h_4 and h5h_5 hold the post-shift stack depth and overflow address.
  • RESPAN removes Sc(a,h1′,0)S_c(a,h'_1,0) and adds Sc(a′,h1′,0)S_c(a',h'_1,0), replacing the current batch ID while preserving its parent.
  • END removes a continuation or a caller frame according to its entry-kind selectors.

When END executes, ℓ=h5\ell = h_5 indicates a LOOP continuation and t=h6t = h_6 indicates that the operation restores a caller frame. Both selectors are constrained to be boolean and mutually exclusive, while h7h_7 is fixed to zero. If t=0t=0, the operation removes Sc(a,a′,ℓ)S_c(a,a',\ell). If t=1t=1, it removes Sf(a,a′,ctx′,b0′,b1′,fn_hash′)S_f(a,a',ctx',b'_0,b'_1,fn\_hash'), binding the restored caller state to the saved entry.

fend⋅ℓ⋅(ℓ−1)=0,fend⋅t⋅(t−1)=0,fend⋅h7=0,fend⋅ℓ⋅t=0f_{end} \cdot \ell \cdot (\ell - 1) = 0, \qquad f_{end} \cdot t \cdot (t - 1) = 0, \qquad f_{end} \cdot h_7 = 0, \qquad f_{end} \cdot \ell \cdot t = 0

The booleanity and mutual-exclusion constraints have degree 66; the h7h_7 zeroing constraint has degree 55.

Adds have multiplicity +1+1 and removals have multiplicity −1-1. LogUp closure requires every completed block to cancel the entry created when that block began.

Block hash table constraints​

As described previously, when the VM starts executing a new program block, it adds hashes of the block's children to the block hash table. When the VM finishes executing a block, it removes the block's hash from the table. The table is therefore updated by JOIN, SPLIT, LOOP, DYN, DYNCALL, CALL, SYSCALL, and END. SPAN does not affect the table because a basic block has no children.

Define the typed BlockHashTable message

B(hash,parent,is_first,is_loop_body),B(hash,parent,is\_first,is\_loop\_body),

whose encoded payload is [hash[0..4], parent, is_first, is_loop_body]. The two JOIN children are

B([h0,…,h3],a′,1,0)andB([h4,…,h7],a′,0,0).B([h_0,\ldots,h_3],a',1,0) \quad\text{and}\quad B([h_4,\ldots,h_7],a',0,0).

Graphically, this looks like so:

air_decoder_left_right_child

Operation flags gate the following positive contributions:

  • JOIN adds both messages above.
  • SPLIT adds the first message when s0=1s_0=1 and the second when s0=0s_0=0.
  • LOOP adds B([h0,…,h3],a′,0,1)B([h_0,\ldots,h_3],a',0,1) with multiplicity group_count, the number of body executions. Its do-while semantics require at least one body execution. REPEAT adds no block-hash entry; each body END must remove a copy of the hash committed by LOOP.
  • DYN, DYNCALL, CALL, and SYSCALL add B([h0,…,h3],a′,0,0)B([h_0,\ldots,h_3],a',0,0).

When END operation is executed, the hash of the completed block is removed from the block hash table. We differentiate between the first and second child of a JOIN by looking at the next opcode: if the next operation is not END, REPEAT, RESPAN, or HALT, then this block is the first child and is_first_child = 1. RESPAN is included defensively — no single constraint forbids END → RESPAN, so excluding it would let an adversarial trace inject a false-positive is_first_child = 1. The is_loop_body flag is read from h4h_4.

Thus, END removes

B([h0,…,h3],a′,1−(fend′+frepeat′+frespan′+fhalt′),h4).B([h_0,\ldots,h_3],a',1-(f'_{end}+f'_{repeat}+f'_{respan}+f'_{halt}),h_4).

Adds have multiplicity +1+1 and the END removal has multiplicity −1-1. The verifier also adds B(program_hash,0,0,0)B(program\_hash,0,0,0) as a boundary contribution. At the root END, the local constraints reduce the removal to that same message, binding the decoded execution to the public program hash.

Basic block​

Basic block constraints ensure proper decoding of basic blocks. In addition to the block stack table constraints and block hash table constraints described previously, decoding of basic blocks requires constraints described below.

In-span column constraints​

The in_span column (denoted as spsp) marks rows which execute non-control flow operations. This is enforced by the control-flow constraint 1−sp−fctrl=01 - sp - f_{ctrl} = 0, so sp=1sp = 1 for non-control flow operations and sp=0sp = 0 otherwise. Semantically, this means spsp is 1 throughout basic blocks and 0 on control-flow rows. The transition constraints below enforce the legal state transitions into and out of basic blocks. Here fctrlf_{ctrl} includes SPAN, JOIN, SPLIT, LOOP, END, REPEAT, RESPAN, HALT, DYN, DYNCALL, CALL, and SYSCALL.

We require that the VM starts outside a basic block. Since sp=1−fctrlsp = 1 - f_{ctrl} and fctrlf_{ctrl} is binary, spsp is also binary.

sp=0 on the first rowsp = 0 \text{ on the first row}

On each transition, SPAN and RESPAN are followed by an in-span row. An in-span row remains inside unless the next operation is END or RESPAN:

sp′=fspan+frespan+sp⋅(1−fend′−frespan′)sp' = f_{span} + f_{respan} + sp \cdot (1 - f_{end}' - f_{respan}')

The first-row constraint above handles the start of the trace separately from this transition rule.

Block address constraints​

When we are inside a basic block, values in block address columns (denoted as aa) must remain the same. The same is true when executing REPEAT, which re-enters the same loop parent for another body iteration. This can be enforced with the following constraint:

(sp+frepeat)⋅(a′−a)=0 | degree=5(sp + f_{repeat}) \cdot (a' - a) = 0 \text{ | degree} = 5

This preservation constraint covers in-span rows and REPEAT. For the remaining control-flow operations, the prover supplies the next block address subject to their operation-specific constraints. In particular, RESPAN increments aa by 11, as specified above.

When an in-span row is followed by END, this constraint copies aa into the END row, keeping the address of the current span batch.

Group count constraints​

The group_count column (denoted as gcgc) is used to keep track of the number of operation groups which remains to be executed in a basic block.

In the beginning of a basic block (i.e., when SPAN operation is executed), the prover sets the value of gcgc non-deterministically. This value is subsequently decremented according to the rules described below. By the time we exit the basic block (i.e., when END operation is executed), the value in gcgc must be 00.

The rules for decrementing values in the gcgc column are as follows:

  • The count cannot be decremented by more than 11 in a single row.
  • When an operation group is fully executed (which happens when h0=0h_0 = 0 inside a basic block), the count is decremented by 11.
  • When SPAN, RESPAN, or PUSH operations are executed, the count is decremented by 11.

Note that these rules imply that the PUSH operation (or any operation with an immediate value) cannot be the last operation in an operation group (otherwise the count would have to be decremented by 22).

To simplify the description of the constraints, we will define the following variable:

Δgc=gc−gc′\Delta gc = gc - gc'

Using this variable, we can describe the constraints against the gcgc column as follows:

Inside a basic block, group count can either stay the same or decrease by one:

sp⋅Δgc⋅(Δgc−1)=0 | degree=3sp \cdot \Delta gc \cdot (\Delta gc - 1) = 0 \text{ | degree} = 3

When group count is decremented inside a basic block, either h0h_0 must be 00 (we consumed all operations in a group) or we must be executing an operation with an immediate value:

sp⋅Δgc⋅(1−fimm)⋅h0=0 | degree=8sp \cdot \Delta gc \cdot (1 - f_{imm})\cdot h_0 = 0 \text{ | degree} = 8

Notice that the above constraint does not preclude fimm=1f_{imm} = 1 and h0=0h_0 = 0 from being true at the same time. If this happens, op group decoding constraints (described here) will force that the operation following the operation with an immediate value is a NOOP.

When executing a SPAN, a RESPAN, or an operation with an immediate value, group count must be decremented by 11:

(fspan+frespan+fimm)⋅(Δgc−1)=0 | degree=6(f_{span} + f_{respan} + f_{imm}) \cdot (\Delta gc - 1) = 0 \text{ | degree} = 6

If the next operation is either an END or a RESPAN, group count must remain the same:

Δgc⋅(fend′+frespan′)=0 | degree=5\Delta gc \cdot (f_{end}' + f_{respan}') = 0 \text{ | degree} = 5

When an END operation is executed, group count must be 00:

fend⋅gc=0 | degree=5f_{end} \cdot gc = 0 \text{ | degree} = 5

Op group decoding constraints​

Inside a basic block, register h0h_0 is used to keep track of operations to be executed in the current operation group. The value of this register is set by the prover non-deterministically at the time when the prover executes a SPAN or a RESPAN operation, or when processing of a new operation group within a batch starts. The picture below illustrates this.

air_decoder_op_group_constraint

In the above:

  • The prover sets the value of h0h_0 non-deterministically at row 00. The value is set to an operation group containing operations op0 through op8.
  • As we start executing the group, at every row we "remove" the least significant operation from the group. This can be done by subtracting opcode of the operation from the group, and then dividing the result by 272^7.
  • By row 99 the group is fully executed. This decrements the group count and set op_index to 00 (constraints against op_index column are described in the next section).
  • At row 1010 we start executing the next group with operations op9 through op11. In this case, the prover populates h0h_0 with the group having its first operation (op9) already removed, and sets the op_bits registers to the value encoding op9.
  • By row 1212 this group is also fully executed.

To simplify the description of the constraints, we define the following variables:

op=∑i=06(bi⋅2i)fsgc=sp⋅sp′⋅(1−Δgc)op = \sum_{i=0}^6 (b_i \cdot 2^i) f_{sgc} = sp \cdot sp' \cdot (1 - \Delta gc)

opop is just an opcode value implied by the values in op_bits registers. fsgcf_{sgc} is a flag which is set to 11 when the group count within a basic block does not change. We multiply it by sp′sp' to make sure the flag is 00 when we are about to end decoding of an operation batch. Note that fsgcf_{sgc} flag is mutually exclusive with fspanf_{span}, frespanf_{respan}, and fimmf_{imm} flags as these three operations decrement the group count.

Using these variables, we can describe operation group decoding constraints as follows:

When a SPAN, a RESPAN, or an operation with an immediate value is executed or when the group count does not change, the value in h0h_0 should be decremented by the value of the opcode in the next row.

(fspan+frespan+fimm+fsgc)⋅(h0−h0′⋅27−op′)=0 | degree=6(f_{span} + f_{respan} + f_{imm} + f_{sgc}) \cdot (h_0 - h_0' \cdot 2^7 - op') = 0 \text{ | degree} = 6

Notice that when the group count does change, and we are not executing fspanf_{span}, frespanf_{respan}, or fimmf_{imm} operations, no constraints are placed against h0h_0, and thus, the prover can populate this register non-deterministically.

When we are in a basic block and the next operation is END or RESPAN, the current value in h0h_0 column must be 00.

sp⋅(fend′+frespan′)⋅h0=0 | degree=6sp \cdot (f_{end}' + f_{respan}') \cdot h_0 = 0 \text{ | degree} = 6

Op index constraints​

The op_index column (denoted as oxox) tracks index of an operation within its operation group. It is used to ensure that the number of operations executed per group never exceeds 99. The index is zero-based, and thus, the possible set of values for oxox is between 00 and 88 (both inclusive).

To simplify the description of the constraints, we will define the following variables:

ng=Δgc−fimmΔox=ox′−oxng = \Delta gc - f_{imm} \Delta ox = ox' - ox

The value of ngng is set to 11 when we are about to start executing a new operation group (i.e., group count is decremented but we did not execute an operation with an immediate value). Using these variables, we can describe the constraints against the oxox column as follows.

When executing SPAN or RESPAN operations the next value of op_index must be set to 00:

(fspan+frespan)⋅ox′=0 | degree=6(f_{span} + f_{respan}) \cdot ox' = 0 \text{ | degree} = 6

When starting a new operation group inside a basic block, the next value of op_index must be set to 00. Note that we multiply by spsp to exclude the cases when the group count is decremented because of SPAN or RESPAN operations:

sp⋅ng⋅ox′=0 | degree=6sp \cdot ng \cdot ox' = 0 \text{ | degree} = 6

When inside a basic block but not starting a new operation group, op_index must be incremented by 11. Note that we multiply by sp′sp' to exclude the cases when we are about to exit processing of an operation batch (i.e., the next operation is either END or RESPAN):

sp⋅sp′⋅(1−ng)⋅(Δox−1)=0 | degree=7sp \cdot sp' \cdot (1 - ng) \cdot (\Delta ox - 1) = 0 \text{ | degree} = 7

Values of op_index must be in the range [0,8][0, 8].

∏i=08(ox−i)=0 | degree=9\prod_{i=0}^{8}(ox - i) = 0 \text{ | degree} = 9

Op batch encoding constraints​

The full_batch and batch_size_code columns specify how many operation groups are present in an operation batch. Let ff denote full_batch, let tt denote batch_size_code, and let A=fspan+frespanA = f_{span} + f_{respan}. The valid encodings are:

Encoding (f,t)(f, t)Meaning
(1,0)(1, 0)8 groups
(0,1)(0, 1)4 groups
(0,−1)(0, -1)2 groups
(0,0)(0, 0) with A=1A = 11 group
(0,0)(0, 0) with A=0A = 0Inactive row

The following constraints restrict the pair to these encodings. The two inactive-row constraints directly pin each committed value to zero when the encoding is inactive.

f(f−1)=0 | degree=2f(f - 1) = 0 \text{ | degree} = 2
t(t−1)(t+1)=0 | degree=3t(t - 1)(t + 1) = 0 \text{ | degree} = 3
f⋅t=0 | degree=2f \cdot t = 0 \text{ | degree} = 2
(1−A)⋅f=0 | degree=6(1 - A) \cdot f = 0 \text{ | degree} = 6
(1−A)⋅t=0 | degree=6(1 - A) \cdot t = 0 \text{ | degree} = 6

The four batch-size selectors are derived as follows:

  • fg8=ff_{g8} = f.
  • fg4=(t2+t)/2f_{g4} = (t^2 + t) / 2.
  • fg2=(t2−t)/2f_{g2} = (t^2 - t) / 2.
  • fg1=A−f−t2f_{g1} = A - f - t^2.

Their degrees are 11, 22, 22, and 55, respectively. They sum to AA, so the shared (0,0)(0, 0) encoding selects one group on SPAN and RESPAN rows and selects no group on inactive rows.

Design rationale​

The value −1-1 is simply the field value assigned to the 2-group case; this encoding does not use signed integer arithmetic. The values −1-1, 00, and 11 are distinct in the trace field, and the constraint t(t−1)(t+1)=0t(t - 1)(t + 1) = 0 permits exactly those values.

The 8-group case gets its own Boolean flag because it controls the largest op-group lookup. The remaining three active sizes share the short-batch code. The product constraint f⋅t=0f \cdot t = 0 excludes mixed encodings, while the two separate inactive-row constraints ensure that neither value can be non-zero when the encoding is inactive.

When we have at most 4 groups in a batch, registers h4,...,h7h_4, ..., h_7 should be set to 00's.

(A−f)⋅hi=0 for i∈[4,8) | degree=6(A - f) \cdot h_i = 0 \text{ for } i \in [4, 8) \text{ | degree} = 6

When we have at most 2 groups in a batch, registers h2h_2 and h3h_3 should also be set to 00's.

(fg1+fg2)⋅hi=0 for i∈2,3 | degree=6(f_{g1} + f_{g2}) \cdot h_i = 0 \text{ for } i \in 2, 3 \text{ | degree} = 6

When we have 1 group in a batch, register h1h_1 should also be set to 00.

fg1⋅h1=0 | degree=6f_{g1} \cdot h_1 = 0 \text{ | degree} = 6

Op group table constraints​

Op group table is used to ensure that all operation groups in a given batch are consumed before a new batch is started (i.e., via a RESPAN operation) or the execution of a basic block is complete (i.e., via an END operation). The op group table is updated according to the following rules:

An entry is the typed message

G(batch_id,group_pos,group_value)=Pop_group+batch_id+βgroup_pos+β2group_value.G(batch\_id,group\_pos,group\_value) = P_{op\_group} + batch\_id + \beta group\_pos + \beta^2 group\_value.

When a SPAN or a RESPAN operation is executed, we compute the values of the rows to be added to the op group table as follows:

G(a′,gc−i,hi),i∈[1,8).G(a',gc-i,h_i), \qquad i \in [1,8).

The batch-size flags select the required prefix of these messages: zero for a one-group batch, one for a two-group batch, three for a four-group batch, or seven for an eight-group batch. Each selected message is added with multiplicity +1+1. The batch address comes from the next row's block address a′a'.

When a deferred group or immediate is consumed, the decoder removes

G(a,gc,fpushs0′+(1−fpush)(27h0′+op′)).G(a,gc,f_{push}s'_0+(1-f_{push})(2^7h'_0+op')).

For PUSH, the group value is the immediate at s0′s'_0. Otherwise it combines h0′h'_0 with the next opcode. The batch address and group position come from the current aa and gcgc values.

We also define a flag which is set to 11 when a group needs to be removed from the op group table.

fdg=sp⋅Δgc | degree=2f_{dg} = sp \cdot \Delta gc \text{ | degree} = 2

Thus, a group is removed with multiplicity −1-1 exactly when its count is decremented inside a basic block. Multiplying by spsp excludes the decrements performed by SPAN and RESPAN. LogUp closure requires every deferred group added at batch setup to be consumed.