Miden VM Program decoder
Miden VM program decoder is responsible for ensuring that a program with a given MAST root is executed by the VM. As the VM executes a program, the decoder does the following:
- Decodes a sequence of field elements supplied by the prover into individual operation codes (or opcodes for short).
- Organizes the sequence of field elements into code blocks, and computes the hash of the program according to the methodology described here.
At the end of program execution, the decoder outputs the computed program hash. This hash binds the sequence of opcodes executed by the VM to a program the prover claims to have executed. The verifier uses this hash during the STARK proof verification process to verify that the proof attests to a correct execution of a specific program (i.e., the prover didn't claim to execute program while in fact executing a different program ).
The sections below describe how Miden VM decoder works. Throughout these sections we make the following assumptions:
- An opcode requires bits to represent.
- An immediate value requires one full field element to represent.
- A
NOOPoperation has a numeric value of , and thus, can be encoded as seven zeros. Executing aNOOPoperation does not change the state of the VM, but it does advance operation counter, and may affect program hash.
Program execution
Miden VM programs consist of a set of code blocks organized into a binary tree. The leaves of the tree contain linear sequences of instructions, and control flow is defined by the internal nodes of the tree.
Managing control flow in the VM is accomplished by executing control flow operations listed in the table below. Each of these operations requires exactly one VM cycle to execute.
| Operation | Description |
|---|---|
JOIN | Initiates processing of a new Join block. |
SPLIT | Initiates processing of a new Split block. |
LOOP | Initiates processing of a new Loop block. |
REPEAT | Initiates a new iteration of an executing loop. |
SPAN | Initiates processing of a new basic block. |
RESPAN | Initiates processing of a new operation batch within a basic block. |
DYN | Initiates processing of a new Dyn block. |
DYNCALL | Initiates processing of a new Dyncall block. |
CALL | Initiates processing of a new Call block. |
SYSCALL | Initiates processing of a new Syscall block. |
END | Marks the end of a program block. |
HALT | Marks the end of the entire program. |
Let's consider a simple program below:
begin
<operations1>
if.true
<operations2>
else
<operations3>
end
end
Block structure of this program is shown below.
JOIN
SPAN
<operations1>
END
SPLIT
SPAN
<operations2>
END
SPAN
<operations3>
END
END
END
Executing this program on the VM can result in one of two possible instruction sequences. First, if after operations in <operations1> are executed the top of the stack is , the VM will execute the following:
JOIN
SPAN
<operations1>
END
SPLIT
SPAN
<operations2>
END
END
END
HALT
However, if after <operations1> are executed, the top of the stack is , the VM will execute the following:
JOIN
SPAN
<operations1>
END
SPLIT
SPAN
<operations3>
END
END
END
HALT
The main task of the decoder is to output exactly the same program hash, regardless of which one of the two possible execution paths was taken. However, before we can describe how this is achieved, we need to give an overview of the overall decoder structure.
Decoder structure
The decoder is one of the more complex parts of the VM. It consists of the following components:
- Main execution trace consisting of trace columns which contain the state of the decoder at a given cycle of a computation.
- Connection to the hash chiplet, which is used to offload hash computations from the decoder.
- virtual tables, implemented with typed LogUp messages, which keep track of code blocks and operations executing on the VM.
Decoder trace
Decoder trace columns are grouped as follows:
| Columns | Registers | Purpose |
|---|---|---|
| Block address | ||
| -- | Opcode bits | |
| -- | Hasher state and operation helpers | |
| In-basic-block flag | ||
Remaining operation-group count, or body multiplicity on LOOP rows | ||
| Index within the current operation group | ||
full_batch | Full eight-group batch indicator | |
batch_size_code | Short-batch size code (, , or ) | |
| -- | Degree-reduction registers |
These registers have the following meanings:
- Block address register . This register contains the hash-controller row address for the current block. It also serves the role of unique block identifiers. This is convenient, because hasher addresses are guaranteed to be unique.
- Registers , which encode opcodes for operation to be executed by the VM. Each of these registers can contain a single binary value (either or ). And together these values describe a single opcode.
- Hasher registers . When control flow operations are executed, these registers are used to provide inputs for the current block's hash computation (e.g., for
JOIN,SPLIT,LOOP,SPAN,CALL,SYSCALLoperations) or to record the result of the hash computation (i.e., forENDoperation). However, when regular operations are executed, of these registers are used to help with op group decoding, and the remaining can be used to hold operation-specific helper variables. - Register which contains a binary flag indicating whether the VM is executing instructions inside a basic block. The flag is set to when the VM executes non-control flow instructions, and is set to otherwise.
- Register which keeps track of the number of unprocessed operation groups in a given basic block.
- Register which keeps track of a currently executing operation's index within its operation group.
- Operation batch registers
full_batchandbatch_size_code, which encode how many operation groups a batch contains. The first is binary, the second is in , and each is constrained to zero on every other opcode. - Two additional registers and are used primarily for constraint degree reduction.
Program block hashing
To compute program-block hashes, the decoder relies on the hash controller. It performs two related Eidos operations:
- A two-to-one hash compresses one 8-element block under a domain-specific chaining value and returns a 4-element digest.
- A sequential hash uses one controller row per 8-element block. The digest of each row becomes the input chaining value of the next row.
Each controller row contains both the compression input and output. A typed compression-link
message binds its complete (block, cv_in, cv_out) tuple to one physical 32-row cycle in
EidosCompressionAir. Identical tuples may share one provider cycle through multiplicity without
removing any decoder/controller request.
Simple two-to-one hash
The decoder initializes the Eidos chaining value from the control-block opcode and places the two
child words in the block. At controller address r, it consumes a full-state input message and a
digest-return message. Both messages use node index zero and the same row address. The explicit
bus domains distinguish the input and output even though they share an address.
Sequential hash
The first block contributes a full-state input message at address r. Each following block
contributes a next-block absorption message at the next controller address. The controller AIR
constrains that row's input chaining value to equal the previous row's output chaining value.
The final block also supplies the digest-return message at its own address. Thus, for a stream of
k blocks, controller addresses run from r through r + k - 1, and the result is returned at
r + k - 1.
Message reduction and the cross-AIR compression link are described in the
hash-chiplet lookup section.
Control flow tables
In addition to the hash chiplet, control-flow operations use three virtual tables: the block stack, block hash, and op group tables. Each table is encoded as a domain-separated typed LogUp relation: insertions contribute with multiplicity , and removals with multiplicity . The following sections define their payloads and update rules.
Block stack table
When the VM starts executing a new program block, it adds its block ID together with the ID of its parent block (and some additional info) to the block stack table. When a program block is fully executed, it is removed from the table. In this way, the table represents a stack of blocks which are currently executing on the VM. By the time program execution completes, block stack table must be empty.
The block stack table is also used to ensure that execution contexts are managed properly across
the CALL, DYNCALL, and SYSCALL operations.
Each BlockStackTable message has an 11-slot payload. Continuations use
while caller frames use
The final slot authenticates the entry kind. It distinguishes a caller frame even when every
saved caller-state value is zero. A caller frame saves the caller context, stack depth, overflow
address, and function hash for restoration at END. Starting a block adds its message; completing
it removes the same message. LogUp closure binds the restored values to those saved on entry.
Block hash table
When the VM starts executing a new program block, it adds hashes of the block's children to the block hash table. And when the VM finishes executing a block, it removes its hash from the block hash table. Thus, by the time program execution completes, block hash table must be empty.
The table can be thought of as consisting of columns as shown below:

where:
- The first four columns () contain the block hash.
- The next column () contains its parent ID, or for the program root.
- is for the first child of a join block.
- is for the body of a loop.
Each entry is a BlockHashTable message with the payload
Unlike other virtual tables, block hash table does not start out in an empty state. Specifically, it is initialized with a single row containing the hash of the program's root block. This needs to be done because the root block does not have a parent and, thus, otherwise it would never be added to the block hash table.
The verifier supplies that root entry as a positive LogUp boundary contribution. The root END
row removes the matching message, binding the decoded execution to the public program hash.
Op group table
Op group table is used in decoding of basic blocks, which are leaves in a program's MAST. As described here, a basic block can contain one or more operation batches, each batch containing up to operation groups.
When the VM starts executing a new batch of operations, it adds all operation groups within a batch, except for the first one, to the op group table. Then, as the VM starts executing an operation group, it removes the group from the table. Thus, by the time all operation groups in a batch have been executed, the op group table must be empty.
The table can be thought of as consisting of columns as shown below:

The meaning of the columns is as follows:
- The first column () contains operation batch ID. During the execution of the program, each operation batch is assigned a unique ID.
- The second column () contains the position of the group in the basic block (not just in the current batch). The position is -based and is counted from the end. Thus, for example, if a basic block consists of a single batch with groups, the position of the first group would be , the position of the second group would be etc. (the reason for this is explained in this section). Note that the group with position is not added to the table, because it is the first group in the batch, so the first row of the table will be for the group with position .
- The third column () contains the actual values of operation groups (this could include up to opcodes or a single immediate value).
Each entry is an OpGroupTable message with the payload
Batch setup adds every deferred group, while beginning a group or consuming an immediate removes the matching message. The table is empty at completion because the relation's signed LogUp sum must close.
Control flow operation semantics
In this section we describe high-level semantics of executing all control flow operations. The descriptions are not meant to be complete and omit some low-level details. However, they provide good intuition on how these operations work.
JOIN operation
Before a JOIN operation is executed by the VM, the prover populates registers with hashes of left and right children of the join program block as shown in the diagram below.

In the above diagram, blk is the ID of the join block which is about to be executed. blk is also the hash-controller row address. prnt is the ID of the block's parent.
When the VM executes a JOIN operation, it does the following:
- Adds a continuation
(blk, prnt, 0)to the block stack table. - Adds tuples
(blk, left_child_hash, 1, 0)and(blk, right_child_hash, 0, 0)to the block hash table. - Initiates a 2-to-1 hash computation in the hash chiplet (as described here) using
blkas the hash-controller row address and as input values.
SPLIT operation
Before a SPLIT operation is executed by the VM, the prover populates registers with hashes of true and false branches of the split program block as shown in the diagram below.

In the above diagram, blk is the ID of the split block which is about to be executed. blk is also the hash-controller row address. prnt is the ID of the block's parent.
When the VM executes a SPLIT operation, it does the following:
- Adds a continuation
(blk, prnt, 0)to the block stack table. - Pops the stack and:
a. If the popped value is , adds a tuple(blk, true_branch_hash, 0, 0)to the block hash table.
b. If the popped value is , adds a tuple(blk, false_branch_hash, 0, 0)to the block hash table.
c. If the popped value is neither nor , the execution fails. - Initiates a 2-to-1 hash computation in the hash chiplet (as described here) using
blkas the hash-controller row address and as input values.
LOOP operation
Before a LOOP operation is executed by the VM, the prover populates registers with hash of the loop's body as shown in the diagram below. The remaining registers are set to to complete the 8-element Eidos block.

In the above diagram, blk is the ID of the loop block which is about to be executed. blk is also the hash-controller row address. prnt is the ID of the block's parent.
The LOOP operation has do-while semantics: the body is entered unconditionally for the first iteration, with the condition checked only at the end of each iteration by REPEAT/END. When the VM executes a LOOP operation, it does the following:
- Adds a continuation
(blk, prnt, 1)to the block stack table (the1indicates that the loop's body is expected to be executed). - Adds the loop-body hash to the block hash table with multiplicity
group_count, the number of body executions. Each bodyENDremoves one copy. - Initiates a 2-to-1 hash computation in the hash chiplet (as described here) using
blkas the hash-controller row address and the padded input .
The LOOP operation does not read or pop the stack.
SPAN operation
Before a SPAN operation is executed by the VM, the prover populates registers with contents of the first operation batch of the basic block as shown in the diagram below. The prover also sets the group count register to the total number of operation groups in the basic block.

In the above diagram, blk is the ID of the basic block which is about to be executed. blk is also the hash-controller row address. prnt is the ID of the block's parent. g0_op0 is the first operation of the batch, and g_0' is the first operation group of the batch with the first operation removed.
When the VM executes a SPAN operation, it does the following:
- Adds a tuple
(blk, prnt, 0, 0...)to the block stack table. - Adds groups of the operation batch, as specified by the batch encoding (see here) to the op group table.
- Initiates a sequential hash computation in the hash chiplet (as described here) using
blkas the hash-controller row address and as input values. - Sets the
in_spanregister to . - Decrements
group_countregister by . - Sets the
op_indexregister to .
DYN operation

In the above diagram, blk is the ID of the dyn block which is about to be executed. blk is also the hash-controller row address. p_addr is the ID of the block's parent.
When the VM executes a DYN operation, it does the following:
- Adds a continuation
(blk, p_addr, 0)to the block stack table. - Sends a memory read request to the memory chiplet, using
s0as the memory address. The resulthash of calleeis placed in the decoder hasher trace at . - Adds the tuple
(blk, hash of callee, 0, 0)to the block hash table. - Initiates a 2-to-1 hash computation in the hash chiplet (as described here) using
blkas the hash-controller row address and[ZERO; 8]as the Eidos input block. - Performs a stack left shift
- The new
s15is pulled from the stack overflow table if present; otherwise it is set to0.
- The new
Note that unlike DYNCALL, the ctx and fn_hash registers are unchanged.
DYNCALL operation

In the above diagram, blk is the ID of the dyn block which is about to be executed. blk is also the hash-controller row address. p_addr is the ID of the block's parent.
When the VM executes a DYNCALL operation, it does the following:
- Adds a tuple
(blk, p_addr, 0, ctx, h_4, h_5, fn_hash)to the block stack table. The and registers hold the caller's post-shift stack depth and overflow address;fn_hashis a four-element word. - Sends a memory read request to the memory chiplet, using
s0as the memory address. The resulthash of calleeis placed in the decoder hasher trace at . - Sends a memory write request to the memory chiplet to set address
FMP_ADDR = 2^32 - 2toFMP_INIT_VALUE = 2^31in the new memory context. This initializes thefmpin the new context. - Adds the tuple
(blk, hash of callee, 0, 0)to the block hash table. - Initiates a 2-to-1 hash computation in the hash chiplet (as described here) using
blkas the hash-controller row address and[ZERO; 8]as the Eidos input block. - Performs a stack left shift
- The new
s15is pulled from the stack overflow table if present; otherwise it is set to0.
- The new
Similar to CALL, DYNCALL sets up a new ctx, and sets the fn_hash registers to the callee hash.
END operation
Before an END operation is executed by the VM, the prover populates registers with the hash of the block which is about to end. The prover also sets values in and registers as follows:
- is set to if the block is a body of a loop block. We denote this value as
f0. - is set to if the block is a loop block. We denote this value as
f1. Under do-while semantics every loop block is entered, so for every loop'sENDrow. - is set to if this
ENDrestores a caller frame created byCALL,DYNCALL, orSYSCALL. We denote this value asf2. - is set to on every
ENDrow. In the diagram below this isf3.

In the above diagram, blk is the ID of the block which is about to finish executing. prnt is the ID of the block's parent.
When the VM executes an END operation, it does the following:
- Removes a tuple from the block stack table.
- if
f2is set, we remove a caller-frame row(blk, prnt, 0, ctx_next, b0_next, b1_next, fn_hash_next, 1)- in the above, the
x_nextvariables denote the columnxin the next row
- in the above, the
- otherwise, we remove a continuation row
(blk, prnt, f1, 0, 0, 0, [0; 4], 0)
- if
- Removes a tuple
(prnt, current_block_hash, nxt, f0)from the block hash table, where if the next operation isEND,REPEAT,RESPAN, orHALT, and otherwise. - Reads the block digest from the hash chiplet (as described here) using
blkas the hash-controller row address. - If (i.e., we are exiting a loop block), pops the value off the top of the stack and verifies that the value is .
- Verifies that
group_countregister is set to .
HALT operation
Before a HALT operation is executed by the VM, the VM copies values in registers to the next row as illustrated in the diagram below:

In the above diagram, blk is the ID of the block which is about to finish executing.
When the VM executes a HALT operation, it does the following:
- Verifies that block address register is set to .
- If we are not at the last row of the trace, verifies that the next operation is
HALT. - Copies values of registers to the next row.
- Populates all other decoder registers with 's in the next row.
REPEAT operation
REPEAT immediately follows an END whose identifies the completed node as a loop body.
The decoder AIR checks that preceding flag and the parent address. The helper values shown on the
REPEAT row below are not used to add a block-hash entry.

In the above diagram, blk is the ID of the loop's body and prnt is the ID of the loop.
The REPEAT operation pops a loop condition of from the operand stack. The next
iteration removes another copy of the body hash from the weighted entry created by LOOP.
RESPAN operation
Before a RESPAN operation is executed by the VM, the VM copies the ID of the current block blk and the number of remaining operation groups in the basic block to the next row, and sets the value of in_span column to . The prover also sets the value of register for the next row to the ID of the current block's parent prnt as shown in the diagram below:

In the above diagram, g0_op0 is the first operation of the new operation batch, and g0' is the first operation group of the batch with g0_op0 operation removed.
When the VM executes a RESPAN operation, it does the following:
- Increments block address by .
- Removes the tuple
(blk, prnt, 0, 0...)from the block stack table. - Adds the tuple
(blk+1, prnt, 0, 0...)to the block stack table. - Absorbs values in registers into the hasher state of the hash chiplet (as described here).
- Sets the
in_spanregister to . - Adds groups of the operation batch, as specified by the batch encoding (see here) to the op group table using
blk+1as batch ID.
The net result of the above is that we incremented the ID of the current block by (the next controller row) and added the next set of operation groups to the op group table.
CALL operation
Recall that the purpose of a CALL operation is to execute a procedure in a new execution context. Specifically, this means that the entire memory is zero'd in the new execution context, and the stack is truncated to a depth of 16 (i.e. any element in the stack overflow table is not available in the new context). On the corresponding END instruction, the prover will restore the previous execution context (verified by the block stack table).
Before a CALL operation, the prover populates registers with the hash of the procedure being called. In the next row, the prover
- sets the context ID to the next row's CLK value
- sets the
fn hashregisters to the hash of the callee- This register is what the
callerinstruction uses to return the hash of the caller
- This register is what the
- resets the stack
B0register to 16 (which tracks the current stack depth) - resets the overflow address to 0 (which tracks the "address" of the last element added to the overflow table)
- it is set to 0 to indicate that the overflow table is empty

In the above diagram, blk is the ID of the call block which is about to be executed. blk is also the hash-controller row address. prnt is the ID of the block's parent.
When the VM executes a CALL operation, it does the following:
- Adds a tuple
(blk, prnt, 0, p_ctx, p_b0, p_b1, prnt_fn_hash)to the block stack table. - Initiates a 2-to-1 hash computation in the hash chiplet (as described here) using
blkas the hash-controller row address and as input values. - Sends a memory write request to the memory chiplet to set address
FMP_ADDR = 2^32 - 2toFMP_INIT_VALUE = 2^31in the new memory context. This initializes thefmpin the new context.
SYSCALL operation
Similarly to the CALL operation, a SYSCALL changes the execution context. However, it always jumps back to the root context, and executes kernel procedures only.
Before a SYSCALL operation, the prover populates registers with the hash of the procedure being called. In the next row, the prover
- sets the context ID to 0,
- does NOT modify the
fn hashregister- Hence, the
fn hashregister contains the procedure hash of the caller, to be accessed by thecallerinstruction,
- Hence, the
- resets the stack
B0register to 16 (which tracks the current stack depth) - resets the overflow address to 0 (which tracks the "address" of the last element added to the overflow table)
- it is set to 0 to indicate that the overflow table is empty

In the above diagram, blk is the ID of the syscall block which is about to be executed. blk is also the hash-controller row address. prnt is the ID of the block's parent.
When the VM executes a SYSCALL operation, it does the following:
- Adds a tuple
(blk, prnt, 0, p_ctx, p_b0, p_b1, prnt_fn_hash)to the block stack table. - Sends a request to the kernel ROM chiplet indicating that
hash of calleeis being accessed.- this results in a fault if
hash of calleedoes not correspond to the hash of a kernel procedure
- this results in a fault if
- Initiates a 2-to-1 hash computation in the hash chiplet (as described here) using
blkas the hash-controller row address and as input values.
Program decoding
When decoding a program, we start at the root block of the program. We can compute the hash of the root block directly from hashes of its children. The prover provides hashes of the child blocks non-deterministically, and we use them to compute the program's hash (here we rely on the hash chiplet). We then verify the program hash via boundary constraints. Thus, if the prover provided valid hashes for the child blocks, we will get the expected program hash.
Now, we need to verify that the VM executed the child blocks correctly. We do this recursively similar to what is described above: for each of the blocks, the prover provides hashes of its children non-deterministically and we verify that the hash has been computed correctly. We do this until we get to the leaf nodes (i.e., basic blocks). Hashes of basic blocks are computed sequentially from the instructions executed by the VM.
The sections below illustrate how different types of code blocks are decoded by the VM.
JOIN block decoding
When decoding a join bock, the VM first executes a JOIN operation, then executes the first child block, followed by the second child block. Once the children of the join block are executed, the VM executes an END operation. This is illustrated in the diagram below.

As described previously, when the VM executes a JOIN operation, hashes of both children are added to the block hash table. These hashes are removed only when the END operations for the child blocks are executed. Thus, until both child blocks are executed, the block hash table is not cleared.
SPLIT block decoding
When decoding a split block, the decoder pops an element off the top of the stack, and if the popped element is , executes the block corresponding to the true branch. If the popped element is , the decoder executes the block corresponding to the false branch. This is illustrated on the diagram below.

As described previously, when the VM executes a SPLIT operation, only the hash of the branch to be executed is added to the block hash table. Thus, until the child block corresponding to the required branch is executed, the block hash table is not cleared.
LOOP block decoding
A loop block has do-while semantics: the body is entered unconditionally for the first iteration, and the trailing condition the body leaves on top of the stack determines whether the VM executes another iteration (REPEAT) or exits (END).
When the VM executes a LOOP operation, it adds one weighted body-hash entry whose multiplicity
equals the number of body executions. It also adds a row to the block stack table with is_loop
set to . The LOOP operation itself does not read or pop the stack.
Each execution of the body ends with an END that removes one unit of the weighted body-hash
entry. If the trailing condition is , the VM executes REPEAT and enters the body again; a
REPEAT with a trailing condition of fails. Since REPEAT adds no block-hash entry, each
iteration must execute the body that LOOP committed to.
This process is illustrated on the diagram below.

The above steps are repeated until the top of the stack becomes , at which point the VM executes the END operation. Since is_loop is always for a loop's END row under do-while semantics, the END operation always pops the trailing condition off the stack. If the popped value is not , the operation fails. Thus, the VM can exit the loop block only when the top of the stack is .
DYN block decoding
When decoding a dyn bock, the VM first executes a DYN operation, then executes the child block dynamically specified by the top of the stack. Once the child of the dyn block has been executed, the VM executes an END operation. This is illustrated in the diagram below.

As described previously, when the VM executes a DYN operation, the hash of the child is added to the block hash table. This hash is removed only when the END operation for the child block is executed. Thus, until the child block corresponding to the dynamically specified target is executed, the block hash table is not cleared.
Basic block decoding
As described here, a basic block can contain one or more operation batches, each batch containing up to operation groups. At the high level, decoding of a basic block is done as follows:
- At the beginning of the block, we make a request to the hash chiplet which initiates the hasher, absorbs the first operation batch ( field elements) into the hasher, and returns the row address of the hasher, which we use as the unique ID for the basic block (see here).
- We then add groups of the operation batch, as specified by the batch encoding (but always skipping the first one) to the op group table.
- We then remove operation groups from the op group table in the FIFO order one by one, and decode them in the manner similar to the one described here.
- Once all operation groups in a batch have been decoded, we absorb the next batch into the hasher and repeat the process described above.
- Once all batches have been decoded, we return the hash of the basic block from the hasher.
Overall, three control flow operations are used when decoding a basic block:
SPANoperation is used to initialize a hasher and absorbs the first operation batch into it.RESPANoperation is used to absorb any additional batches in the basic block.ENDoperation is used to end the decoding of a basic block and retrieve its hash from the hash chiplet.
Operation group decoding
As described here, an operation group is a sequence of operations which can be encoded into a single field element. For a field element of bits, we can fit up to operations into a group. We do this by concatenating binary representations of opcodes together with the first operation located in the least significant position.
We can read opcodes from the group by simply subtracting them from the op group value and then dividing the result by . Once the value of the op group reaches , we know that all opcodes have been read. Graphically, this can be illustrated like so:

Notice that despite their appearance, op bits is actually separate registers, while op group is just a single register.
We also need to make sure that at most operations are executed as a part of a single group. For this purpose we use the op_index column. Values in this column start out at for each operation group, and are incremented by for each executed operation. To make sure that at most operations can be executed in a group, the value of the op_index column is not allowed to exceed .
Operation batch encoding
Two operation batch registers specify how many operation groups comprise a given operation batch. For most batches, the number of groups will be equal to . However, for the last batch in a block (or for the first batch, if the block consists of only a single batch), the number of groups may be less than . Since processing of new batches starts only on SPAN and RESPAN operations, the encoding is active only for these operations.
To simplify the constraint system, the number of groups in a batch can be only one of the following values: , , , and . If the number of groups in a batch does not match one of these values, the batch is padded with NOOP operations, one per added group. For example, a three-group batch is padded to four groups. Since the numeric value of NOOP is , a zero-valued operation group represents the padding operation.
The pair encodes the batch size and determines which groups are added to the op group table:
| Encoding | Batch size | Groups added to the op group table |
|---|---|---|
on SPAN or RESPAN | None | |
| otherwise | Inactive | None |
Thus, the constrained SPAN/RESPAN selector distinguishes a one-group batch from an inactive row; both use the same two committed values.
Single-batch span
The simplest example of a basic block is a block with a single batch. This batch may contain up to operation groups (e.g., ). Decoding of such a block is illustrated in the diagram below.

Before the VM starts processing this basic block, the prover populates registers with operation groups . The prover also puts the total number of groups into the group_count register . In this case, the total number of groups is .
When the VM executes a SPAN operation, it does the following:
- Initiates hashing of elements using hash chiplet. The hasher address is used as the block ID
blk, and it is inserted intoaddrregister in the next row. - Adds a continuation
(blk, prnt, 0)to the block stack table. - Sets the
is_spanregister to in the next row. - Sets the
op_indexregister to in the next row. - Decrements
group_countregister by . - Sets
op bitsregisters at the next step to the first operation of , and also copies with the first operation removed (denoted as ) to the next row. - Adds groups to the op group table. Thus, after the
SPANoperation is executed, op group table looks as shown below.

Then, with every step the next operation is removed from , and by step , the value of is . Once this happens, the VM does the following:
- Decrements
group_countregister by . - Sets
op bitsregisters at the next step to the first operation of . - Sets
hasherregister to the value of with the first operation removed (denoted as ). - Removes row
(blk, 7, g1)from the op group table. This row can be obtained by taking values from registers:addr,group_count, and for , where and refer to values in the next row for the first hasher column andop_bitscolumns respectively.
Note that we rely on the group_count column to construct the row to be removed from the op group table. Since group count is decremented from the total number of groups to , to remove groups from the op group table in correct order, we need to assign group position to groups in the op group table in the reverse order. For example, the first group to be removed should have position , the second group to be removed should have position etc.
Decoding of is performed in the same manner as decoding of : with every subsequent step the next operation is removed from until its value reaches , at which point, decoding of group begins.
The above steps are executed until value of group_count reaches . Once group_count reaches and the last operation group is executed, the VM executes the END operation. Semantics of the END operation are described here.
Notice that by the time we get to the END operation, all rows are removed from the op group table.
Multi-batch basic block
A basic block may contain an unlimited number of operation batches. As mentioned previously, to absorb a new batch into the hasher, the VM executes a RESPAN operation. The diagram below illustrates decoding of a basic block consisting of two operation batches.

Decoding of such a block will look very similar to decoding of the single-basic block described previously, but there also will be some differences.
First, after the SPAN operation is executed, the op group table will look as follows:

Notice that while the same groups () are added to the table, their positions now reflect the total number of groups in the basic block.
Second, RESPAN advances the hasher controller address by , because each absorbed batch occupies one controller row.
Advancing the addr register changes the ID of the basic block. The block-stack entry therefore changes from (blk, prnt, 0) to (blk + 1, prnt, 0). The prover sets in the next row to prnt to perform this update.
Executing a RESPAN operation also adds groups to the op group table, which now would look as follows:

Then, the execution of the second batch proceeds in a manner similar to the first batch: we remove operations from the current op group, execute them, and when the value of the op group reaches , we start executing the next group in the batch. Thus, by the time we get to the END operation, the op group table should be empty.
When END executes, it reads the basic-block digest from the final controller row at addr; after one RESPAN, this is blk + 1.
Handling immediate values
Miden VM operations can carry immediate values. Currently, the only such operation is a PUSH operation. Since immediate values can be thought of as constants embedded into program code, we need to make sure that changing immediate values affects program hash.
To achieve this, we treat immediate values in a manner similar to how we treat operation groups. Specifically, when computing hash of a basic block, immediate values are absorbed into the hasher state in the same way as operation groups are. As mentioned previously, an immediate value is represented by a single field element, and thus, an immediate value takes place of a single operation group.
The diagram below illustrates decoding of a basic block with operations one of which is a PUSH operation.

In the above, when the SPAN operation is executed, immediate value imm0 will be added to the op group table, which will look as follows:

Then, when the PUSH operation is executed, the VM will do the following:
- Decrement
group_countby . - Remove a row from the op group table equal to
(addr, group_count, s0'), where is the value of the top of the stack at the next row (i.e., it is the value that is pushed onto the stack).
Thus, after the PUSH operation is executed, the op group table is cleared, and group count decreases to (which means that there are no more op groups to execute). Decoding of the rest of the op group proceeds as described in the previous sections.
Program decoding example
Let's run through an example of decoding a simple program shown previously:
begin
<operations1>
if.true
<operations2>
else
<operations3>
end
end
Translating this into code blocks with IDs assigned, we get the following:
b0: JOIN
b1: SPAN
<operations1>
b1: END
b2: SPLIT
b3: SPAN
<operations2>
b3: END
b4: SPAN
<operations3>
b4: END
b2: END
b0: END
The root of the program is a join block . This block contains two children: a basic bock and a split block . In turn, the split block contains two children: a basic block and a basic block .
When this program is executed on the VM, the following happens:
- Before the program starts executing, block hash table is initialized with a single row containing the hash of .
- Then,
JOINoperation for is executed. It adds hashes of and to the block hash table. It also adds an entry for to the block stack table. States of both tables after this step are illustrated below. - Then, basic is executed and a sequential hash of its operations is computed. Also, when
SPANoperation for is executed, an entry for is added to the block stack table. At the end of (whenENDis executed), entries for are removed from both the block hash and block stack tables. - Then,
SPLIToperation for is executed. It adds an entry for to the block stack table. Also, depending on whether the top of the stack is or , either hash of or hash of is added to the block hash table. Let's say the top of the stack is . Then, at this point, the block hash and block stack tables will look like in the second picture below. - Then, basic is executed and a sequential hash of its instructions is computed. Also, when
SPANoperation for is executed, an entry for is added to the block stack table. At the end of (whenENDis executed), entries for are removed from both the block hash and block stack tables. - Then,
ENDoperation for is executed. It removes the hash of from the block hash table, and also removes the entry for from the block stack table. The third picture below illustrates the states of block stack and block hash tables after this step. - Then,
ENDfor is executed, which removes entries for from the block stack and block hash tables. At this point both tables are empty. - Finally, a sequence of
HALToperations is executed until the length of the trace reaches a power of two.
States of block hash and block stack tables after step 2:

States of block hash and block stack tables after step 4:

States of block hash and block stack tables after step 6:
