Skip to main content
Version: 0.11 (stable)

Hash chiplet

Miden VM "offloads" all hash-related computations to a separate hash processor. This chiplet supports executing the Rescue Prime Optimized hash function (or rather a specific instantiation of it) in the following settings:

  • A single permutation of Rescue Prime Optimized.
  • 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.

InstructionDescription
HRExecutes a single round of the VM's native hash function. All cycles which are not one less than a multiple of 88 execute this instruction. That is, the chiplet executes this instruction on cycles 0,1,2,3,4,5,60, 1, 2, 3, 4, 5, 6, but not 77, and then again, 8,9,10,11,12,13,148, 9, 10, 11, 12, 13, 14, but not 1515 etc.
BPInitiates computation of a single permutation, a 2-to-1 hash, or a linear hash of many elements. This instruction can be executed only on cycles which are multiples of 88, and it can also be executed concurrently with an HR instruction.
MPInitiates Merkle path verification computation. This instruction can be executed only on cycles which are multiples of 88, and it can also be executed concurrently with an HR instruction.
MVInitiates Merkle path verification for the "old" node value during Merkle root update computation. This instruction can be executed only on cycles which are multiples of 88, and it can also be executed concurrently with an HR instruction.
MUInitiates Merkle path verification for the "new" node value during Merkle root update computation. This instruction can be executed only on cycles which are multiples of 88, and it can also be executed concurrently with an HR instruction.
HOUTReturns the result of the currently running computation. This instruction can be executed only on cycles which are one less than a multiple of 88 (e.g. 77, 1515 etc.).
SOUTReturns the whole hasher state. This instruction can be executed only on cycles which are one less than a multiple of 88, and only if the computation was started using BP instruction.
ABPAbsorbs a new set of elements into the hasher state when computing a linear hash of many elements. This instruction can be executed only on cycles which are one less than a multiple of 88, and only if the computation was started using BP instruction.
MPAAbsorbs the next Merkle path node into the hasher state during Merkle path verification computation. This instruction can be executed only on cycles which are one less than a multiple of 88, and only if the computation was started using MP instruction.
MVAAbsorbs the next Merkle path node into the hasher state during Merkle path verification for the "old" node value during Merkle root update computation. This instruction can be executed only on cycles which are one less than a multiple of 88, and only if the computation was started using MV instruction.
MUAAbsorbs the next Merkle path node into the hasher state during Merkle path verification for the "new" node value during Merkle root update computation. This instruction can be executed only on cycles which are one less than a multiple of 88, and only if the computation was started using Mu instruction.

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 88 table rows. The layout of the table is illustrated below.

hash_execution_trace

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 88 rows. For k0k_0 the pattern is 77 zeros followed by 11 one, helping us identify the last row in the cycle. For k1k_1 the pattern is 66 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 77 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 four columns (h0,...,h3h_0, ..., h_3) are reserved for capacity elements of the state. When the state is initialized for hash computations, h0h_0 should be set to 00 if the number of elements to be hashed is a multiple of the rate width (88). Otherwise, h0h_0 should be set to 11. h1h_1 should be set to the domain value if a domain has been provided (as in the case of control block hashing). All other capacity elements should be set to 00's.
    • The next eight columns (h4,...,h11h_4, ..., h_{11}) are reserved for the rate elements of the state. These are used to absorb the values to be hashed. Once the permutation is complete, hash output is located in the first four rate columns (h4,...,h7h_4, ..., h_7).
  • 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 77 steps of every 88-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 88.
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 88.
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 88.
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 88.
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 88.
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 88.
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 88.
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 88.
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 88.
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 88.

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 Rescue Prime Optimized hash function involves the following steps:

  1. Initialize hasher state with 1212 field elements.
  2. Apply Rescue Prime Optimized 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 // execute 6 more hash rounds
SOUT // return the entire state as output

Execution trace for this computation would look as illustrated below.

hash_1_permutation_trace

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 Rescue Prime Optimized permutation.
  3. Return elements [4,8)[4, 8) 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 // execute 6 more hash rounds
HOUT // return elements 4, 5, 6, 7 of the state as output

Execution trace for this computation would look as illustrated below.

hash_2_to_1_hash

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 Rescue Prime Optimized 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 [4,8)[4, 8) 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 // execute 6 more hash rounds
ABP // absorb the next set of elements into the state
HR HR HR HR HR HR HR // execute 7 hash rounds
HOUT // return elements 4, 5, 6, 7 of the state as output

Execution trace for this computation would look as illustrated below.

hash_linear_hash_n

In the above, the value absorbed into hasher state between rows 77 and 88 is the delta between values tit_i and sis_i. Thus, if we define bi=tisib_i = t_i - s_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 Rescue Prime Optimized 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 [4,8)[4, 8) 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 // execute 6 more hash rounds
MPA // copy result & absorb the next node into the state
HR HR HR HR HR HR HR // execute 7 hash rounds
HOUT // return elements 4, 5, 6, 7 of the state as output

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.

hash_merkle_tree_trace

In the above, the prover provides values for nodes cc and ee non-deterministically.

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 // execute 6 more hash rounds
MPV // copy result & absorb the next node into the state
HR HR HR HR HR HR HR // execute 7 hash rounds
HOUT // return elements 4, 5, 6, 7 of the state as output

// verify the new merkle path
[MU, HR] // init state and execute a hash round (concurrently)
HR HR HR HR HR HR // execute 6 more hash rounds
MPU // copy result & absorb the next node into the state
HR HR HR HR HR HR HR // execute 7 hash rounds
HOUT // return elements 4, 5, 6, 7 of the state as output

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 8 (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 88, 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 77 row of every 88-row cycle (i.e., when k0=0k_0=0), we need to apply Rescue Prime Optimized round constraints to the hasher state. For brevity, we omit these constraints from this note.
  • On the 88th row of every 88-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 first 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(hjhj)=0 | degree=5f_{abp} \cdot (h'_j - h_j) = 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 (h4,...,h7h_4, ..., h_7) are copied over either to (h4,...,h7)(h_4', ..., h_7') or to (h8,...,h11)(h_8', ..., h_{11}') 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)(hj+4hj+4)+b(hj+8hj+4))=0 | degree=6(f_{mp} + f_{mv} + f_{mu}) \cdot ((1 - b) \cdot (h_{j +4}' - h_{j+4}) + b \cdot (h_{j + 8}' - h_{j + 4})) = 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 88 or 11 less than a multiple of 88. 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+24k0+25k2vh=α0+α1m+α2(clk+1)+α3iva=j=03(αj+4hj)vb=j=47(αj+4hj)vc=j=811(αj+4hj)vd=j=811(αjhj)m = op_{label} + 2^4 \cdot k_0 + 2^5 \cdot k_2 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=4}^{7}(\alpha_{j+4} \cdot h_j) v_c = \sum_{j=8}^{11}(\alpha_{j+4} \cdot h_j) v_d = \sum_{j=8}^{11}(\alpha_j \cdot h_j)

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 first, second, and third words (4 elements) of the hasher state.
  • vdv_d is the third word of the hasher state but computed using the same α\alpha values as used for the second word. This is needed for computing the value of vleafv_{leaf} below to ensure that the same α\alpha values are used for the leaf node regardless of which part of the state the node comes from.

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)vb+bvdv_{leaf} = v_h + (1-b) \cdot v_b + 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 deltas between the last 88 elements of the hasher state (the rate) into bchipb_{chip}:

vabp=vh+vb+vc(vb+vc)v_{abp} = v_h + v'_b + v'_c - (v_b + v_c)

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

vres=vh+vbv_{res} = v_h + v_b

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 vbv_b and vcv_c defined above and define the value representing an entry in the sibling table as follows:

vsibling=α0+α3i+bvb+(1b)vcv_{sibling} = \alpha_0 + \alpha_3 \cdot i + b \cdot v_b + (1-b) \cdot v_c

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.