Range Checker
Miden VM relies heavily on 16-bit range checks, which prove that a field element represents an integer in . Selected u32 operations request checks for four helper values; U32DIV requests two additional checks. Each active memory row requests five checks. MPVERIFY and MRUPDATE request two checks for their Merkle-path depth, and each Merkle path leg requests five checks for its canonical-index witness.
Thus, it is very important for the VM to be able to perform a large number of 16-bit range checks very efficiently. In this note we describe how this can be achieved using the LogUp lookup argument.
8-bit range checks
First, let's define a construction for the simplest possible 8-bit range-check. This can be done with a single column as illustrated below.

For this to work as a range-check we need to enforce a few constraints on this column:
- The value in the first row must be .
- The value in the last row must be .
- As we move from one row to the next, we can either keep the value the same or increment it by .
Denoting as the value of column in the current row, and as the value of column in the next row, we can enforce the last condition as follows:
Together, these constraints guarantee that all values in column are between and (inclusive).
We can then make use of the LogUp lookup argument by adding another column which will keep a running sum that is the logarithmic derivative of the product of values in the column. The transition constraint for would look as follows:
Since constraints cannot include divisions, the constraint would actually be expressed as the following degree 2 constraint:
Using these two columns we can check if some other column in the execution trace is a permutation of values in . Let's call this other column . We can compute the logarithmic derivative for as a running sum in the same way as we compute it for . Then, we can check that the last value in is the same as the final value for the running sum of .
While this approach works, it has a couple of limitations:
- First, column must contain all values between and . Thus, if column does not contain one of these values, we need to artificially add this value to somehow (i.e., we need to pad with extra values).
- Second, assuming is the length of execution trace, we can range-check at most values. Thus, if we wanted to range-check more than values, we'd need to introduce another column similar to .
We can get rid of both requirements by including the multiplicity of the value into the calculation of the logarithmic derivative for LogUp, which will allow us to specify exactly how many times each value needs to be range-checked.
A better construction
Let's add one more column to our table to keep track of how many times each value should be range-checked.

The transition constraint for is now as follows:
This addresses the limitations we had as follows:
- We no longer need to pad the column we want to range-check with extra values because we can skip the values we don't care about by setting the multiplicity to .
- Repeated checks of the same value do not require additional table rows; they only increase that value's multiplicity. The number of distinct values remains bounded by the available table rows.
Additionally, the constraint degree has not increased versus the naive approach, and the only additional cost is a single trace column.
16-bit range checks
To support 16-bit range checks, let's try to extend the idea of the 8-bit table. Our 16-bit table would look like so (the only difference is that column now has to end with value ):

While this works, it is rather wasteful. In the worst case, we'd need to enumerate over 65K values, most of which we may not actually need. It would be nice if we could "skip over" the values that we don't want. One way to do this could be to add bridge rows between two values to be range checked and add constraints to enforce the consistency of the gap between these bridge rows.
If we allow gaps between two consecutive rows to only be 0 or powers of 2, we could enforce a constraint:
This constraint has a degree 9. This construction allows the minimum trace length to be 1024.
We could go even further and allow the gaps between two consecutive rows to only be 0 or powers of 3. In this case we would enforce the constraint:
This allows us to reduce the minimum trace length to 64.
To find out the number of bridge rows to be added in between two values to be range checked, we represent the gap between them as a linear combination of powers of 3, ie,
Starting from the current value, we add one bridge row for each power-of-three step in this decomposition except the final step, which lands on the next requested value. A coefficient therefore uses two steps of size .
Miden approach
This construction is implemented in Miden with the following requirements, capabilities and constraints.
Requirements
- 2 columns of the main trace: , where contains the value being range-checked and is the number of times the value is checked (its multiplicity).
- 1 domain-separated communication bus,
RangeCheck, to ensure that the table multiplicities match the requests from u32 operations, Merkle operations, and the memory chiplet.
Capabilities
The construction gives us the following capabilities:
- A table with enough rows can contain every 16-bit value and therefore serve any range-check request produced by the execution trace.
- With fewer rows, the number of distinct requested values is limited by the requested-value rows and the bridge rows between them. Repeated requests for an existing value consume no additional rows because they are aggregated into its multiplicity.
Execution trace
The range checker's execution trace looks as follows:

The columns have the following meanings:
- is the multiplicity column that indicates the number of times the value in that row should be range checked (included into the computation of the logarithmic derivative).
- contains the values to be range checked.
- The first value is and the last value is .
- Consecutive values must either stay the same or increase by a power of 3 no greater than .
Execution trace constraints
First, we need to constrain that the consecutive values in the range checker are either the same or differ by powers of 3 that are less than or equal to .
In addition to the transition constraints described above, we also need to enforce the following boundary constraints:
- The value of in the first row is .
- The value of in the last row is .
Communication bus
The domain-separated RangeCheck communication bus connects components that require 16-bit checks to the range table. It encodes a value as . A request made under flag contributes
while a range-table row contributes
The current requesters are:
- Selected
u32operations, which request checks for four decoder helper values.U32DIVrequests two additional checks for its remainder bound. MPVERIFYandMRUPDATE, which request checks for the depth and the scaled value . Together these enforce .- Each MPVERIFY path and each of MRUPDATE's old and new paths, which request checks for the four limbs of the level-0 canonical-index slack and for . The limb checks give , and the extra check gives . Thus the complete slack is less than ; see Merkle range checks.
- The memory chiplet, which requests five checks per active row for the delta limbs and and the word-address values , , and .
These interactions do not use a dedicated accumulator. They are packed with other lookup interactions in the Core and Chiplets AIRs. For AIR , let be the sum of all its lookup contributions and its trace length. The AIR commits the normalized sum
The verifier enforces the cross-AIR identity
where contains the explicit boundary messages required by other buses. The RangeCheck bus has no boundary messages, so its table responses must cancel its requests. Internally, the first lookup accumulator is anchored at zero and follows a normalized cyclic recurrence, including the last-to-first edge; there is no separate requirement that a terminal value be zero.