Merkle Prover
This section explains the prover of the Merkle commitment scheme, focusing on how columns are committed to compute a Merkle root and how the Merkle tree layers are constructed, as well as how to generate Merkle proofs (decommitments). S-two represents the computation trace using multiple columns of different sizes. Rather than committing to each column in separate Merkle trees, S-two uses a modified Merkle tree structure where all columns of different sizes are committed to using a single Merkle tree. We will describe the structure of this Merkle tree in this section.MerkleProver Structure
TheMerkleProver struct represents a prover for a Merkle commitment scheme. It stores all layers of the Merkle tree, where each layer contains the hash values at that level.
The Commit Process
The core of the Merkle prover is thecommit function, which takes as input a set of columns and outputs a MerkleProver containing all layers of the Merkle tree. The columns must be of length that is a power of 2.
Below is the complete implementation of the commit function:
- Sort Columns by Length: Columns are sorted in descending order of length (columns with the most elements appear first). This ensures that the largest columns are committed first.
-
Layer-by-Layer Commitment:
- For each layer (from largest to smallest), the function collects all columns of the current size and computes the hashes for that layer using the
commit_on_layerfunction. - For the largest layer, the previous layer’s hashes are empty, so the hash is computed directly from the column values.
- For subsequent layers, the hash is computed from the previous layer’s hashes and the current layer’s column values.
- For each layer (from largest to smallest), the function collects all columns of the current size and computes the hashes for that layer using the
- Reverse Layers: After all layers are computed, the list of layers is reversed so that the root layer is at the beginning.
Example: Commit Process
Suppose the input column data is as shown below:- First Layer (Leaves): The hashes are computed directly from the column values:
- Second Layer: The next layer uses the previous hashes and the values from :
- Root: The root is computed as .
The Decommit Process
The decommitment process enables the prover to generate a Merkle proof for a set of queried indices, allowing the verifier to check that specific elements are included in the committed Merkle tree. The output is aMerkleDecommitment struct, which contains the hash and column values required for the verifier to reconstruct the Merkle root at the queried positions. Its implementation is shown below:
decommit function implemented for the MerkleProver takes as input:
queries_per_log_size: A map from log size to a vector of query indices for columns of that size.columns: The column data that was committed to in the Merkle tree.
- A vector of queried values, ordered as they are opened (from largest to smallest layer).
- A
MerkleDecommitmentcontaining the hash and column witnesses needed for verification.
decommit function:
-
Sort Columns by Length:
- As in the
commitfunction, columns are sorted in descending order of length.
- As in the
-
Layer-by-Layer Decommitment:
- For each layer (from largest to smallest):
- Collect all columns of the current size (
layer_columns) and the previous layer’s hashes (previous_layer_hashes). - Retrieve the queries for the current layer (
layer_column_queries) and the previous layer’s queries (prev_layer_queries). - For each node index to be decommitted in this layer:
- Check if the child node hashes of the current node can be computed by the verifier, and if not, add the missing child node hashes to the
hash_witnessin the decommitment. - If the node index is queried, fetch the corresponding column values and append them to
queried_values. - If not queried, add the column values to the
column_witnessin the decommitment.
- Check if the child node hashes of the current node can be computed by the verifier, and if not, add the missing child node hashes to the
- The set of node indices decommitted in this layer is propagated as queries to the next layer.
- Collect all columns of the current size (
- For each layer (from largest to smallest):
Example: Decommit Process
For the column data in Figure 1, consider the query indices , where the query indices are maps from log size to a vector of query indices for columns of that size. This corresponds to querying the following elements:- The 0th element of columns of size : from and from .
- The 1st element of the column of size : from .
decommit function, showing the state of key variables for each layer_log_size:
-
layer_log_size = 2 (columns of size 4):
layer_columns:previous_layer_hashes:None(first layer)prev_layer_queries: emptylayer_column_queries:- For
node_index = 0:node_values:- Queried, so append to
queried_values: - Add
node_index = 0tolayer_total_queries(to propagate to next layer)
- At this stage:
queried_values= ,decommitmentis empty.
-
layer_log_size = 1 (columns of size 2):
layer_columns:previous_layer_hashes:prev_layer_queries:layer_column_queries:- For
node_index = 0:- Add to
hash_witnessin decommitment. node_values:- Not queried, so append to
column_witnessin decommitment. - Add
node_index = 0tolayer_total_queries.
- Add to
- At this stage:
queried_values= ,hash_witness= ,column_witness= ,layer_total_queries= . - For
node_index = 1:- Add to
hash_witnessin decommitment. node_values:- Queried, so append to
queried_values. - Add
node_index = 1tolayer_total_queries.
- Add to
- At this stage:
queried_values= ,hash_witness= ,column_witness= ,layer_total_queries= (to propagate to next layer).
-
layer_log_size = 0 (root):
layer_columns: emptyprevious_layer_hashes:prev_layer_queries:layer_column_queries: empty- No values are added to
queried_values,hash_witness, orcolumn_witnessin this layer.
queried_values:decommitment:hash_witness:column_witness: