Using statically allocated structures
zkLLVM only supports data structures that are statically allocated on the stack instead of being dynamically allocated on the heap.
This means that a structure must have a fixed size known at compile time.
For C++, the following types would be supported as they are allocated on the stack.
- std::array<T, N>
- std::pair<U, V>
In contrast, structures such as std::vector<T> and std::map<Key, T> are unsupported.
For Rust, the below types are supported.
- [T; N]
- (T, N, ..., U, V)
However, Vec<T> and similar structures are unsupported.
Consider the following examples in which fixed size structures are used to build a four-layer Merkle tree.
- C++
- Rust
#include <nil/crypto3/hash/algorithm/hash.hpp>
#include <nil/crypto3/hash/poseidon.hpp>
using namespace nil::crypto3;
using namespace nil::crypto3::algebra::curves;
[[circuit]] bool merkle_tree_poseidon (
    typename pallas::base_field_type::value_type expected_root,
    [[private_input]] std::array<typename pallas::base_field_type::value_type, 0x20> layer_0_leaves) {
        std::array<typename pallas::base_field_type::value_type, 0x10> layer_1_leaves;
        constexpr std::size_t layer_1_size = layer_1_leaves.size();
        std::array<typename pallas::base_field_type::value_type, 0x8> layer_2_leaves;
        constexpr std::size_t layer_2_size = layer_2_leaves.size();
        std::array<typename pallas::base_field_type::value_type, 0x4> layer_3_leaves;
        constexpr std::size_t layer_3_size = layer_3_leaves.size();
        std::array<typename pallas::base_field_type::value_type, 0x2> layer_4_leaves;
        constexpr std::size_t layer_4_size = layer_4_leaves.size();
        typename pallas::base_field_type::value_type root;
    for (std::size_t leaf_index = 0; leaf_index < layer_1_size; leaf_index++) {
        layer_1_leaves[leaf_index] =
            hash<hashes::poseidon>(layer_0_leaves[2 * leaf_index], layer_0_leaves[2 * leaf_index + 1]);
    }
    for (std::size_t leaf_index = 0; leaf_index < layer_2_size; leaf_index++) {
        layer_2_leaves[leaf_index] =
            hash<hashes::poseidon>(layer_1_leaves[2 * leaf_index], layer_1_leaves[2 * leaf_index + 1]);
    }
    for (std::size_t leaf_index = 0; leaf_index < layer_3_size; leaf_index++) {
        layer_3_leaves[leaf_index] =
            hash<hashes::poseidon>(layer_2_leaves[2 * leaf_index], layer_2_leaves[2 * leaf_index + 1]);
    }
    for (std::size_t leaf_index = 0; leaf_index < layer_4_size; leaf_index++) {
        layer_4_leaves[leaf_index] =
            hash<hashes::poseidon>(layer_3_leaves[2 * leaf_index], layer_3_leaves[2 * leaf_index + 1]);
    }
    typename pallas::base_field_type::value_type real_root = hash<hashes::poseidon>(layer_4_leaves[0], layer_4_leaves[1]);
    bool res = (real_root == expected_root);
    __builtin_assigner_exit_check(res);
    return res;
}
#![no_main]
use std::intrinsics::assigner_sha2_256;
use ark_ff::AdditiveGroup;
use ark_pallas::Fq;
use unroll::unroll_for_loops;
type BlockType = [Fq; 2];
#[circuit]
#[unroll_for_loops]
pub fn balance(layer_0_leaves: [BlockType; 0x20], expected_root: Fq) -> bool {
    let mut layer_1_leaves: [BlockType; 0x10] = [[Fq::ZERO, Fq::ZERO]; 0x10];
    let mut layer_2_leaves: [BlockType; 0x8] = [[Fq::ZERO, Fq::ZERO]; 0x8];
    let mut layer_3_leaves: [BlockType; 0x4] = [[Fq::ZERO, Fq::ZERO]; 0x4];
    let mut layer_4_leaves: [BlockType; 0x2] = [[Fq::ZERO, Fq::ZERO]; 0x2];
    for leaf_index in 0..10 {
        layer_1_leaves[leaf_index] = hash(
            layer_0_leaves[2 * leaf_index],
            layer_0_leaves[2 * leaf_index + 1],
        );
    }
    for leaf_index in 0..8 {
        layer_2_leaves[leaf_index] = hash(
            layer_1_leaves[2 * leaf_index],
            layer_1_leaves[2 * leaf_index + 1],
        );
    }
    for leaf_index in 0..4 {
        layer_3_leaves[leaf_index] = hash(
            layer_2_leaves[2 * leaf_index],
            layer_2_leaves[2 * leaf_index + 1],
        );
    }
    for leaf_index in 0..2 {
        layer_4_leaves[leaf_index] = hash(
            layer_3_leaves[2 * leaf_index],
            layer_3_leaves[2 * leaf_index + 1],
        );
    }
    let real_root: BlockType = hash(layer_4_leaves[0], layer_4_leaves[1]);
    let res: bool = real_root == expected_root;
    unsafe {
        std::intrinsics::assigner_exit_check(res);
    }
    res
}
fn hash(block1: BlockType, block2: BlockType) -> BlockType {
    let sha = assigner_sha2_256([block1[0].0, block1[1].0], [block2[0].0, block2[1].0]);
    [sha[0].into(), sha[1].into()]
}
The example demonstrates that even complex data structures such as Merkle trees can be implemented from scratch using const size containers. When declaring a dynamic size container, consider the use case and replace it with a const size container if possible.