Skip to main content
Version: 0.14 (unstable)

Hash chiplet

Miden VM "offloads" all hash-related computations to a separate hash processor. This chiplet supports executing the Poseidon2 hash function in the following settings:

  • A single permutation of Poseidon2.
  • A simple 2-to-1 hash.
  • A linear hash of nn field elements.
  • Merkle path verification.
  • Merkle root update.

The chiplet can be thought of as having a small instruction set of 1111 instructions. These instructions are listed below, and examples of how these instructions are used by the chiplet are described in the following sections.

InstructionDescriptionCyclesContextNotes
HRExecutes a single round of the VM's native hash function00-3030, 3232-6262, 6464-9494... (not 3131, 6363, 9595...)Any
BPInitiates computation of a single permutation, a 2-to-1 hash, or a linear hash of many elementsMultiples of 3232 (00, 3232, 6464...)Start of computationConcurrent with HR
MPInitiates Merkle path verification computationMultiples of 3232Start of computationConcurrent with HR
MVInitiates Merkle path verification for the "old" node valueMultiples of 3232Merkle root updateConcurrent with HR
MUInitiates Merkle path verification for the "new" node valueMultiples of 3232Merkle root updateConcurrent with HR
HOUTReturns the "output" portion of the hasher state (indices [0,4)[0,4))32n132n-1 (3131, 6363, 9595...)End of computation
SOUTReturns entire hasher state32n132n-1 (3131, 6363, 9595...)End of computationOnly after BP
ABPAbsorbs a new set of elements into the hasher state32n132n-1 (3131, 6363, 9595...)Linear hash (multi-block)Only after BP
MPAAbsorbs the next Merkle path node into the hasher state32n132n-1 (3131, 6363, 9595...)Merkle path verificationOnly after MP
MVAAbsorbs the next Merkle path node into the hasher state during Merkle path verification for the "old" node value32n132n-1 (3131, 6363, 9595...)Merkle root updateOnly after MV
MUAAbsorbs the next Merkle path node into the hasher state during Merkle path verification for the "new" node value32n132n-1 (3131, 6363, 9595...)Merkle root updateOnly after MU

Chiplet trace

Execution trace table of the chiplet consists of 1616 trace columns and 33 periodic columns. The structure of the table is such that a single permutation of the hash function can be computed using 3232 table rows. The layout of a single 32-row cycle is summarized below (rows omitted are identical permutation rows).

Row (mod 32)k2k_2k1k_1k0k_0s0,s1,s2s_0,s_1,s_2RATE0 (h0..h3h_0..h_3)RATE1 (h4..h7h_4..h_7)CAP (h8..h11h_8..h_{11})ii
0100op-specificinput rate0input rate1input capacityindex
1000op-specificpermutation statepermutation statepermutation stateindex
...........................
30010op-specificpermutation statepermutation statepermutation stateindex
31001op-specificstate (post-permutation)state (post-permutation)state (post-permutation)index

The meaning of the columns is as follows:

  • Three periodic columns k0k_0, k1k_1, and k2k_2 are used to help select the instruction executed at a given row. All of these columns contain patterns which repeat every 3232 rows. For k0k_0 the pattern is 3131 zeros followed by 11 one, helping us identify the last row in the cycle. For k1k_1 the pattern is 3030 zeros, 11 one, and 11 zero, which can be used to identify the second-to-last row in a cycle. For k2k_2 the pattern is 11 one followed by 3131 zeros, which can identify the first row in the cycle.
  • Three selector columns s0s_0, s1s_1, and s2s_2. These columns can contain only binary values (ones or zeros), and they are also used to help select the instruction to execute at a given row.
  • Twelve hasher state columns h0,...,h11h_0, ..., h_{11}. These columns are used to hold the hasher state for each round of the hash function permutation. The state is laid out as follows:
    • The first eight columns (h0,...,h7h_0, ..., h_7) are reserved for the rate elements of the state, arranged as two 4-element words (RATE0, RATE1). Once the permutation is complete, the hash output is located in the first rate word (h0,...,h3h_0, ..., h_3).
    • The last four columns (h8,...,h11h_8, ..., h_{11}) are reserved for capacity elements of the state. When the state is initialized for hash computations, h8h_8 should be set to 00 if the number of elements to be hashed is a multiple of the rate width (88). Otherwise, h8h_8 should be set to 11. h9h_9 should be set to the domain value if a domain has been provided (as in the case of control block hashing). The remaining capacity lanes (h10h_{10}, h11h_{11}) are set to 00.
  • One index column ii. This column is used to help with Merkle path verification and Merkle root update computations.

In addition to the columns described above, the chiplet relies on two running product columns which are used to facilitate multiset checks (similar to the ones described here). These columns are:

  • bchipb_{chip} - which is used to tie the chiplet table with the main VM's stack and decoder. That is, values representing inputs consumed by the chiplet and outputs produced by the chiplet are multiplied into bchipb_{chip}, while the main VM stack (or decoder) divides them out of bchipb_{chip}. Thus, if the sets of inputs and outputs between the main VM stack and hash chiplet are the same, the value of bchipb_{chip} should be equal to 11 at the start and the end of the execution trace.
  • p1p_1 - which is used to keep track of the sibling table used for Merkle root update computations. Specifically, when a root for the old leaf value is computed, we add an entry for all sibling nodes to the table (i.e., we multiply p1p_1 by the values representing these entries). When the root for the new leaf value is computed, we remove the entries for the nodes from the table (i.e., we divide p1p_1 by the value representing these entries). Thus, if both computations used the same set of sibling nodes (in the same order), the sibling table should be empty by the time Merkle root update procedure completes (i.e., the value of p1p_1 would be 11).

Instruction flags

As mentioned above, chiplet instructions are encoded using a combination of periodic and selector columns. These columns can be used to compute a binary flag for each instruction. Thus, when a flag for a given instruction is set to 11, the chiplet executes this instruction. Formulas for computing instruction flags are listed below.

FlagValueNotes
frprf_{rpr}1k01 - k_0Set to 11 on the first 3131 steps of every 3232-step cycle.
fbpf_{bp}k2s0(1s1)(1s2)k_2 \cdot s_0 \cdot (1 - s_1) \cdot (1 - s_2)Set to 11 when selector flags are (1,0,0)(1, 0, 0) on rows which are multiples of 3232.
fmpf_{mp}k2s0(1s1)s2k_2 \cdot s_0 \cdot (1 - s_1) \cdot s_2Set to 11 when selector flags are (1,0,1)(1, 0, 1) on rows which are multiples of 3232.
fmvf_{mv}k2s0s1(1s2)k_2 \cdot s_0 \cdot s_1 \cdot (1 - s_2)Set to 11 when selector flags are (1,1,0)(1, 1, 0) on rows which are multiples of 3232.
fmuf_{mu}k2s0s1s2k_2 \cdot s_0 \cdot s_1 \cdot s_2Set to 11 when selector flags are (1,1,1)(1, 1, 1) on rows which are multiples of 3232.
fhoutf_{hout}k0(1s0)(1s1)(1s2)k_0 \cdot (1 - s_0) \cdot (1 - s_1) \cdot (1 - s_2)Set to 11 when selector flags are (0,0,0)(0, 0, 0) on rows which are 11 less than a multiple of 3232.
fsoutf_{sout}k0(1s0)(1s1)s2k_0 \cdot (1 - s_0) \cdot (1 - s_1) \cdot s_2Set to 11 when selector flags are (0,0,1)(0, 0, 1) on rows which are 11 less than a multiple of 3232.
fabpf_{abp}k0s0(1s1)(1s2)k_0 \cdot s_0 \cdot (1 - s_1) \cdot (1 - s_2)Set to 11 when selector flags are (1,0,0)(1, 0, 0) on rows which are 11 less than a multiple of 3232.
fmpaf_{mpa}k0s0(1s1)s2k_0 \cdot s_0 \cdot (1 - s_1) \cdot s_2Set to 11 when selector flags are (1,0,1)(1, 0, 1) on rows which are 11 less than a multiple of 3232.
fmvaf_{mva}k0s0s1(1s2)k_0 \cdot s_0 \cdot s_1 \cdot (1 - s_2)Set to 11 when selector flags are (1,1,0)(1, 1, 0) on rows which are 11 less than a multiple of 3232.
fmuaf_{mua}k0s0s1s2k_0 \cdot s_0 \cdot s_1 \cdot s_2Set to 11 when selector flags are (1,1,1)(1, 1, 1) on rows which are 11 less than a multiple of 3232.

A few additional notes about flag values:

  • With the exception of frprf_{rpr}, all flags are mutually exclusive. That is, if one flag is set to 11, all other flats are set to 00.
  • With the exception of frprf_{rpr}, computing flag values involves 33 multiplications, and thus the degree of these flags is 44.
  • We can also define a flag fout=k0(1s0)(1s1)f_{out} = k_0 \cdot (1 - s_0) \cdot (1 - s_1). This flag will be set to 11 when either fhout=1f_{hout}=1 or fsout=1f_{sout}=1 in the current row.
  • We can define a flag fout=k1(1s0)(1s1)f_{out}' = k_1 \cdot (1 - s_0') \cdot (1 - s_1'). This flag will be set to 11 when either fhout=1f_{hout}=1 or fsout=1f_{sout}=1 in the next row.

We also impose the following restrictions on how values in selector columns can be updated:

  • Values in columns s1s_1 and s2s_2 must be copied over from one row to the next, unless fout=1f_{out} = 1 or fout=1f_{out}' = 1 indicating the hout or sout flag is set for the current or the next row.
  • Value in s0s_0 must be set to 11 if fout=1f_{out}=1 for the previous row, and to 00 if any of the flags fabpf_{abp}, fmpaf_{mpa}, fmvaf_{mva}, or fmuaf_{mua} are set to 11 for the previous row.

The above rules ensure that we must finish one computation before starting another, and we can't change the type of the computation before the computation is finished.

Computation examples

Single permutation

Computing a single permutation of Poseidon2 hash function involves the following steps:

  1. Initialize hasher state with 1212 field elements.
  2. Apply Poseidon2 permutation.
  3. Return the entire hasher state as output.

The chiplet accomplishes the above by executing the following instructions:

[BP, HR]                                    // init state and execute a hash round (concurrently)
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
SOUT // return the entire state as output

Execution trace for this computation would look as illustrated below.

Row (mod 32)k2k_2k1k_1k0k_0s0,s1,s2s_0,s_1,s_2RATE0 (h0..h3h_0..h_3)RATE1 (h4..h7h_4..h_7)CAP (h8..h11h_8..h_{11})ii
0100BPa0..a3a_0..a_3a4..a7a_4..a_7a8..a11a_8..a_{11}ii
1000HRpermutepermutepermuteii
...........................
31001SOUTb0..b3b_0..b_3b4..b7b_4..b_7b8..b11b_8..b_{11}ii

In the above {a0,...,a11}\{a_0, ..., a_{11}\} is the input state of the hasher, and {b0,...,b11}\{b_0, ..., b_{11}\} is the output state of the hasher.

Simple 2-to-1 hash

Computing a 2-to-1 hash involves the following steps:

  1. Initialize hasher state with 88 field elements, setting the second capacity element to domaindomain if the domain is provided (as in the case of control block hashing) or else 00, and the remaining capacity elements to 00.
  2. Apply Poseidon2 permutation.
  3. Return elements [0,4)[0, 4) of the hasher state as output.

The chiplet accomplishes the above by executing the following instructions:

[BP, HR]                                    // init state and execute a hash round (concurrently)
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HOUT // return elements 0, 1, 2, 3 of the state as output (the digest)

Execution trace for this computation would look as illustrated below.

Row (mod 32)k2k_2k1k_1k0k_0s0,s1,s2s_0,s_1,s_2RATE0 (h0..h3h_0..h_3)RATE1 (h4..h7h_4..h_7)CAP (h8..h11h_8..h_{11})ii
0100BPa0..a3a_0..a_3b0..b3b_0..b_30,domain,0,00,\,domain,\,0,\,0ii
1000HRpermutepermutepermuteii
...........................
31001HOUTc0..c3c_0..c_3unusedunusedii

In the above, we compute the following:

{c0,c1,c2,c3}hash({a0,a1,a2,a3},{b0,b1,b2,b3})\{c_0, c_1, c_2, c_3\} \leftarrow hash(\{a_0, a_1, a_2, a_3\}, \{b_0, b_1, b_2, b_3\})

Linear hash of n elements

Computing a linear hash of nn elements consists of the following steps:

  1. Initialize hasher state with the first 88 elements, setting the first capacity register to 00 if nn is a multiple of the rate width (88) or else 11, and the remaining capacity elements to 00.
  2. Apply Poseidon2 permutation.
  3. Absorb the next set of elements into the state (up to 88 elements), while keeping capacity elements unchanged.
  4. Repeat steps 2 and 3 until all nn elements have been absorbed.
  5. Return elements [0,4)[0, 4) of the hasher state as output.

The chiplet accomplishes the above by executing the following instructions (for hashing 1616 elements):

[BP, HR]                                    // init state and execute a hash round (concurrently)
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
ABP // absorb the next set of elements into the state
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR HR // execute 11 more hash rounds
HOUT // return elements 0, 1, 2, 3 of the state as output (the digest)

Execution trace for this computation would look as illustrated below.

Row (mod 32)k2k_2k1k_1k0k_0s0,s1,s2s_0,s_1,s_2RATE0 (h0..h3h_0..h_3)RATE1 (h4..h7h_4..h_7)CAP (h8..h11h_8..h_{11})ii
0100BPa0..a3a_0..a_3a4..a7a_4..a_7p,0,0,0p,\,0,\,0,\,0ii
1000HRpermutepermutepermuteii
...........................
31001ABPb0..b3b_0..b_3b4..b7b_4..b_7(carry)ii
32100BPb0..b3b_0..b_3b4..b7b_4..b_7(carry)ii
...........................
63001HOUTr0..r3r_0..r_3unusedunusedii

In the above, the value absorbed into hasher state between rows 3131 and 3232 is the next batch of 88 input elements, which overwrite the rate lanes. Thus, if we define these elements as bib_i for i[0,8)i \in [0, 8), the above computes the following:

{r0,r1,r2,r3}hash(a0,...,a7,b0,...,b7)\{r_0, r_1, r_2, r_3\} \leftarrow hash(a_0, ..., a_7, b_0, ..., b_7)

Verify Merkle path

Verifying a Merkle path involves the following steps:

  1. Initialize hasher state with the leaf and the first node of the path, setting all capacity elements to 00s. a. Also, initialize the index register to the leaf's index value.
  2. Apply Poseidon2 permutation. a. Make sure the index value doesn't change during this step.
  3. Copy the result of the hash to the next row, and absorb the next node of the Merkle path into the hasher state. a. Remove a single bit from the index, and use it to determine how to place the copied result and absorbed node in the state.
  4. Repeat steps 2 and 3 until all nodes of the Merkle path have been absorbed.
  5. Return elements [0,4)[0, 4) of the hasher state as output. a. Also, make sure the index value has been reduced to 00.

The chiplet accomplishes the above by executing the following instructions (for Merkle tree of depth 33):

[MP, HR]                                    // init state and execute a hash round (concurrently)
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
MPA // copy result & absorb the next node into the state
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR HR // execute 11 more hash rounds
HOUT // return elements 0, 1, 2, 3 of the state as output (the digest)

Suppose we have a Merkle tree as illustrated below. This Merkle tree has 44 leaves, each of which consists of 44 field elements. For example, leaf aa consists of elements a0,a1,a2,a3a_0, a_1, a_2, a_3, leaf be consists of elements b0,b1,b2,b3b_0, b_1, b_2, b_3 etc.

hash_merkle_tree

If we wanted to verify that leaf dd is in fact in the tree, we'd need to compute the following hashes:

rhash(e,hash(c,d))r \leftarrow hash(e, hash(c, d))

And if r=gr = g, we can be convinced that dd is in fact in the tree at position 33. Execution trace for this computation would look as illustrated below.

Row (mod 32)k2k_2k1k_1k0k_0s0,s1,s2s_0,s_1,s_2RATE0 (h0..h3h_0..h_3)RATE1 (h4..h7h_4..h_7)CAP (h8..h11h_8..h_{11})ii
0100MPc0..c3c_0..c_3d0..d3d_0..d_30,0,0,00,0,0,033
1000HRpermutepermutepermute33
...........................
31001MPAe0..e3e_0..e_3f0..f3f_0..f_30,0,0,00,0,0,033
32100MPe0..e3e_0..e_3f0..f3f_0..f_30,0,0,00,0,0,011
...........................
63001HOUTg0..g3g_0..g_3unusedunused00

In the above, the prover provides values for nodes cc and ee non-deterministically. The index ii remains constant throughout a permutation and is shifted at the MPA row, so it changes only between cycles.

Update Merkle root

Updating a node in a Merkle tree (which also updates the root of the tree) can be simulated by verifying two Merkle paths: the path that starts with the old leaf and the path that starts with the new leaf.

Suppose we have the same Merkle tree as in the previous example, and we want to replace node dd with node dd'. The computations we'd need to perform are:

rhash(e,hash(c,d))rhash(e,hash(c,d))r \leftarrow hash(e, hash(c, d)) r' \leftarrow hash(e, hash(c, d'))

Then, as long as r=gr = g, and the same values were used for cc and ee in both computations, we can be convinced that the new root of the tree is rr'.

The chiplet accomplishes the above by executing the following instructions:

// verify the old merkle path
[MV, HR] // init state and execute a hash round (concurrently)
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
MVA // copy result & absorb the next node into the state
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR HR // execute 11 more hash rounds
HOUT // return elements 0, 1, 2, 3 of the state as output (the digest)

// verify the new merkle path
[MU, HR] // init state and execute a hash round (concurrently)
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
MUA // copy result & absorb the next node into the state
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR // execute 10 more hash rounds
HR HR HR HR HR HR HR HR HR HR HR // execute 11 more hash rounds
HOUT // return elements 0, 1, 2, 3 of the state as output (the digest)

The semantics of MV and MU instructions are similar to the semantics of MP instruction from the previous example (and MVA and MUA are similar to MPA) with one important difference: MV* instructions add the absorbed node (together with its index in the tree) to permutation column p1p_1, while MU* instructions remove the absorbed node (together with its index in the tree) from p1p_1. Thus, if the same nodes were used during both Merkle path verification, the state of p1p_1 should not change. This mechanism is used to ensure that the same internal nodes were used in both computations.

AIR constraints

When describing AIR constraints, we adopt the following notation: for column xx, we denote the value in the current row simply as xx, and the value in the next row of the column as xx'. Thus, all transition constraints described in this note work with two consecutive rows of the execution trace.

Selector columns constraints

For selector columns, first we must ensure that only binary values are allowed in these columns. This can be done with the following constraints:

s02s0=0 | degree=2s12s1=0 | degree=2s22s2=0 | degree=2s_0^2 - s_0 = 0 \text{ | degree} = 2 s_1^2 - s_1 = 0 \text{ | degree} = 2 s_2^2 - s_2 = 0 \text{ | degree} = 2

Next, we need to make sure that unless fout=1f_{out}=1 or fout=1f_{out}'=1, the values in columns s1s_1 and s2s_2 are copied over to the next row. This can be done with the following constraints:

(s1s1)(1fout)(1fout)=0 | degree=7(s2s2)(1fout)(1fout)=0 | degree=7(s_1' - s_1) \cdot (1 - f_{out}') \cdot (1 - f_{out}) = 0 \text{ | degree} = 7 (s_2' - s_2) \cdot (1 - f_{out}') \cdot (1 - f_{out}) = 0 \text{ | degree} = 7

Next, we need to enforce that if any of fabp,fmpa,fmva,fmuaf_{abp}, f_{mpa}, f_{mva}, f_{mua} flags is set to 11, the next value of s0s_0 is 00. In all other cases, s0s_0 should be unconstrained. These flags will only be set for rows that are 1 less than a multiple of 32 (the last row of each cycle). This can be done with the following constraint:

s0(fabp+fmpa+fmva+fmua)=0 | degree=5s_0' \cdot (f_{abp} + f_{mpa} + f_{mva} + f_{mua})= 0 \text{ | degree} = 5

Lastly, we need to make sure that no invalid combinations of flags are allowed. This can be done with the following constraints:

k0(1s0)s1=0 | degree=3k_0 \cdot (1 - s_0) \cdot s_1 = 0 \text{ | degree} = 3

The above constraints enforce that on every step which is one less than a multiple of 3232, if s0=0s_0 = 0, then s1s_1 must also be set to 00. Basically, if we set s0=0s_0=0, then we must make sure that either fhout=1f_{hout}=1 or fsout=1f_{sout}=1.

Node index constraints

Node index column ii is relevant only for Merkle path verification and Merkle root update computations, but to simplify the overall constraint system, the same constraints will be imposed on this column for all computations.

Overall, we want values in the index column to behave as follows:

  • When we start a new computation, we should be able to set ii to an arbitrary value.
  • When a computation is finished, value in ii must be 00.
  • When we absorb a new node into the hasher state we must shift the value in ii by one bit to the right.
  • In all other cases value in ii should not change.

A shift by one bit to the right can be described with the following equation: i=2i+bi = 2 \cdot i' + b, where bb is the value of the bit which is discarded. Thus, as long as bb is a binary value, the shift to the right is performed correctly, and this can be enforced with the following constraint:

b2b=0b^2 - b = 0

Since we want to enforce this constraint only when a new node is absorbed into the hasher state, we'll define a flag for when this should happen as follows:

fan=fmp+fmv+fmu+fmpa+fmva+fmuaf_{an} = f_{mp} + f_{mv} + f_{mu} + f_{mpa} + f_{mva} + f_{mua}

And then the full constraint would looks as follows:

fan(b2b)=0 | degree=6f_{an} \cdot (b^2 - b) = 0 \text{ | degree} = 6

Next, to make sure when a computation is finished i=0i=0, we can use the following constraint:

fouti=0 | degree=4f_{out} \cdot i = 0 \text{ | degree} = 4

Finally, to make sure that the value in ii is copied over to the next row unless we are absorbing a new row or the computation is finished, we impose the following constraint:

(1fanfout)(ii)=0 | degree=5(1 - f_{an} - f_{out}) \cdot (i' - i) = 0 \text{ | degree} = 5

To satisfy these constraints for computations not related to Merkle paths (i.e., 2-to-1 hash and liner hash of elements), we can set i=0i = 0 at the start of the computation. This guarantees that ii will remain 00 until the end of the computation.

Hasher state constraints

Hasher state columns h0,...,h11h_0, ..., h_{11} should behave as follows:

  • For the first 3131 rows of every 3232-row cycle (i.e., when k0=0k_0=0), we need to apply Poseidon2 round constraints to the hasher state. For brevity, we omit these constraints from this note.
  • On the 3232nd row of every 3232-row cycle, we apply the constraints based on which transition flag is set as described in the table below.

Specifically, when absorbing the next set of elements into the state during linear hash computation (i.e., fabp=1f_{abp} = 1), the last 44 elements (the capacity portion) are carried over to the next row. For j[0,4)j \in [0, 4) this can be described as follows:

fabp(hj+8hj+8)=0 | degree=5f_{abp} \cdot (h'_{j+8} - h_{j+8}) = 0 \text{ | degree} = 5

When absorbing the next node during Merkle path computation (i.e., fmp+fmv+fmu=1f_{mp} + f_{mv} + f_{mu}=1), the result of the previous hash (h0,...,h3h_0, ..., h_3) is copied over either to (h0,...,h3)(h_0', ..., h_3') or to (h4,...,h7)(h_4', ..., h_7') depending on the value of bb, which is defined in the same way as in the previous section. For j[0,4)j \in [0, 4) this can be described as follows:

(fmp+fmv+fmu)((1b)(hjhj)+b(hj+4hj))=0 | degree=6(f_{mp} + f_{mv} + f_{mu}) \cdot ((1 - b) \cdot (h_{j}' - h_{j}) + b \cdot (h_{j + 4}' - h_{j})) = 0 \text{ | degree} = 6

Note, that when a computation is completed (i.e., fout=1f_{out}=1), the next hasher state is unconstrained.

Multiset check constraints

In this sections we describe constraints which enforce updates for multiset check columns bchipb_{chip} and p1p_1. These columns can be updated only on rows which are multiples of 3232 or 11 less than a multiple of 3232. On all other rows the values in the columns remain the same.

To simplify description of the constraints, we define the following variables. Below, we denote random values sent by the verifier after the prover commits to the main execution trace as α0\alpha_0, α1\alpha_1, α2\alpha_2 etc.

m=oplabel+24k2+25k0vh=α0+α1m+α2(clk+1)+α3iva=j=03(αj+4hj)vb=j=03(αj+8hj+4)vc=j=03(αj+12hj+8)vd=j=03(αj+4hj+4)m = op_{label} + 2^4 \cdot k_2 + 2^5 \cdot k_0 v_h = \alpha_0 + \alpha_1 \cdot m + \alpha_2 \cdot (clk + 1) + \alpha_3 \cdot i v_a = \sum_{j=0}^{3}(\alpha_{j+4} \cdot h_j) v_b = \sum_{j=0}^{3}(\alpha_{j+8} \cdot h_{j+4}) v_c = \sum_{j=0}^{3}(\alpha_{j+12} \cdot h_{j+8}) v_d = \sum_{j=0}^{3}(\alpha_{j+4} \cdot h_{j+4})

Message slot layout is fixed: slots 0..30..3 use α4..7\alpha_{4..7}, slots 4..74..7 use α8..11\alpha_{8..11}, and slots 8..118..11 use α12..15\alpha_{12..15}. For partial messages, we place the payload into slots 0..70..7 and set capacity slots 8..118..11 to zero. Leaf/output messages use only slots 0..30..3.

In the above:

  • mm is a transition label, composed of the operation label and the periodic columns that uniquely identify each transition function. The values in the k0k_0 and k2k_2 periodic columns are included to identify the row in the hash cycle where the operation occurs. They serve to differentiate between operations that share selectors but occur at different rows in the cycle, such as BP, which uses oplinhashop_{linhash} at the first row in the cycle to initiate a linear hash, and ABP, which uses oplinhashop_{linhash} at the last row in the cycle to absorb new elements.
  • vhv_h is a common header which is a combination of the transition label, a unique row address, and the node index. For the unique row address, the clk column from the system component is used, but we add 11, because the system's clk column starts at 00.
  • vav_a, vbv_b, vcv_c are the rate0, rate1, and capacity words (4 elements each).
  • vdv_d reuses the rate0 alpha slice on the rate1 word. This is used for Merkle leaf encoding when the right child is selected. For next-row values, vdv_d' is defined the same way using hh'.

Chiplets bus constraints

As described previously, the chiplets bus bchipb_{chip}, implemented as a running product column, is used to tie the hash chiplet with the main VM's stack and decoder. When receiving inputs from or returning results to the stack (or decoder), the hash chiplet multiplies bchipb_{chip} by their respective values. On the other side, when sending inputs to the hash chiplet or receiving results from the chiplet, the stack (or decoder) divides bchipb_{chip} by their values.

In the section below we describe only the hash chiplet side of the constraints (i.e., multiplying bchipb_{chip} by relevant values). We define the values which are to be multiplied into bchipb_{chip} for each operation as follows:

When starting a new simple or linear hash computation (i.e., fbp=1f_{bp}=1) or when returning the entire state of the hasher (fsout=1f_{sout}=1), the entire hasher state is included into bchipb_{chip}:

vall=vh+va+vb+vcv_{all} = v_h + v_a + v_b + v_c

When starting a Merkle path computation (i.e., fmp+fmv+fmu=1f_{mp} + f_{mv} + f_{mu} = 1), we include the leaf of the path into bchipb_{chip}. The leaf is selected from the state based on value of bb (defined as in the previous section):

vleaf=vh+(1b)va+bvdv_{leaf} = v_h + (1-b) \cdot v_a + b \cdot v_d

When absorbing a new set of elements into the state while computing a linear hash (i.e., fabp=1f_{abp}=1), we include the next rate (state slots 0..70..7) into bchipb_{chip}:

vabp=vh+va+vbv_{abp} = v_h + v_a' + v_b'

When a computation is complete (i.e., fhout=1f_{hout}=1), we include the first rate word of the hasher state (the result) into bchipb_{chip}:

vres=vh+vav_{res} = v_h + v_a

Using the above values, we can describe the constraints for updating column bchipb_{chip} as follows.

bchip=bchip((fbp+fsout)vall+(fmp+fmv+fmu)vleaf+fabpvabp+fhoutvres+1(fbp+fmp+fmv+fmu+fabp+fout))b_{chip}' = b_{chip} \cdot ((f_{bp} + f_{sout}) \cdot v_{all} + (f_{mp} + f_{mv} + f_{mu}) \cdot v_{leaf} + f_{abp} \cdot v_{abp} + f_{hout} \cdot v_{res} + 1 - (f_{bp} + f_{mp} + f_{mv} + f_{mu} + f_{abp} + f_{out}))

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

ConditionApplied constraint
fbp=1f_{bp} = 1bchip=bchipvallb_{chip}' = b_{chip} \cdot v_{all}
fsout=1f_{sout} = 1bchip=bchipvallb_{chip}' = b_{chip} \cdot v_{all}
fmp=1f_{mp} = 1bchip=bchipvleafb_{chip}' = b_{chip} \cdot v_{leaf}
fmv=1f_{mv} = 1bchip=bchipvleafb_{chip}' = b_{chip} \cdot v_{leaf}
fmu=1f_{mu} = 1bchip=bchipvleafb_{chip}' = b_{chip} \cdot v_{leaf}
fabp=1f_{abp} = 1bchip=bchipvabpb_{chip}' = b_{chip} \cdot v_{abp}
fhout=1f_{hout} = 1bchip=bchipvresb_{chip}' = b_{chip} \cdot v_{res}
Otherwisebchip=bchipb_{chip}' = b_{chip}

Note that the degree of the above constraint is 77.

Sibling table constraints

Note: Although this table is described independently, it is implemented as part of the chiplets virtual table, which combines all virtual tables required by any of the chiplets into a single master table.

As mentioned previously, the sibling table (represented by running column p1p_1) is used to keep track of sibling nodes used during Merkle root update computations. For this computation, we need to enforce the following rules:

  • When computing the old Merkle root, whenever a new sibling node is absorbed into the hasher state (i.e., fmv+fmva=1f_{mv} + f_{mva} = 1), an entry for this sibling should be included into p1p_1.
  • When computing the new Merkle root, whenever a new sibling node is absorbed into the hasher state (i.e., fmu+fmua=1f_{mu} + f_{mua} = 1), the entry for this sibling should be removed from p1p_1.

To simplify the description of the constraints, we use variables vav_a and vbv_b defined above and define the value representing an entry in the sibling table as follows:

vsib0=α0+α3i+vbvsib1=α0+α3i+vavsibling=(1b)vsib0+bvsib1v_{sib0} = \alpha_0 + \alpha_3 \cdot i + v_b v_{sib1} = \alpha_0 + \alpha_3 \cdot i + v_a v_{sibling} = (1-b) \cdot v_{sib0} + b \cdot v_{sib1}

Using the above value, we can define the constraint for updating p1p_1 as follows:

p1((fmv+fmva)vsibling+1(fmv+fmva))=p1((fmu+fmua)vsibling+1(fmu+fmua))p_1' \cdot \left( (f_{mv} + f_{mva}) \cdot v_{sibling} + 1 - (f_{mv} + f_{mva}) \right) = p_1 \cdot \left( (f_{mu} + f_{mua}) \cdot v_{sibling} + 1 - (f_{mu} + f_{mua}) \right)

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

ConditionApplied constraint
fmv=1f_{mv} = 1p1vsibling=p1p_1' \cdot v_{sibling} = p_1
fmva=1f_{mva} = 1p1vsibling=p1p_1' \cdot v_{sibling} = p_1
fmu=1f_{mu} = 1p1=p1vsiblingp_1' = p_1 \cdot v_{sibling}
fmua=1f_{mua} = 1p1=p1vsiblingp_1' = p_1 \cdot v_{sibling}
Otherwisep1=p1p_1' = p_1

Note that the degree of the above constraint is 77.

To make sure computation of the old Merkle root is immediately followed by the computation of the new Merkle root, we impose the following constraint:

(fbp+fmp+fmv)(1p1)=0 | degree=5(f_{bp} + f_{mp} + f_{mv}) \cdot (1 - p_1) = 0 \text{ | degree} = 5

The above means that whenever we start a new computation which is not the computation of the new Merkle root, the sibling table must be empty. Thus, after the hash chiplet computes the old Merkle root, the only way to clear the table is to compute the new Merkle root.

Together with boundary constraints enforcing that p1=1p_1=1 at the first and last rows of the running product column which implements the sibling table, the above constraints ensure that if a node was included into p1p_1 as a part of computing the old Merkle root, the same node must be removed from p1p_1 as a part of computing the new Merkle root. These two boundary constraints are described as part of the chiplets virtual table constraints.