Skip to main content

Using built-in types

The C++ and Rust dialects provided by clang and rustc respectively contain several built-in types and functions simplifying working with essential concepts in cryptography such as curves and hashes. This tutorial covers how this built-ins can be used in circuit code.

Using built-ins

Using built-ins offers a more convenient syntax compared to importing types and functions from crypto3 or other libraries.

However, using imports ensures that code is reusable between zkLLVM and native C++ or Rust dialects.

Using built-in curves

The C++ and Rust dialects provided by clang and rustc respectively contain several built-in types simplifying working with elliptic curves. Here is a full list of these types.

  • __zkllvm_curve_pallas
  • __zkllvm_curve_vesta
  • __zkllvm_curve_bls12381
  • __zkllvm_curve_curve25519
  • __zkllvm_field_pallas_base
  • __zkllvm_field_pallas_scalar
  • __zkllvm_field_vesta_base
  • __zkllvm_field_vesta_scalar
  • __zkllvm_field_bls12381_base
  • __zkllvm_field_bls12381_scalar
  • __zkllvm_field_curve25519_base
  • __zkllvm_field_curve25519_scalar
Curve fields

Each built-in curve has one base and one scalar type representing the different types of fields in elliptic cryptography.

The below example uses Pallas curve base types in arbitrary calculations.


__zkllvm_field_pallas_base pow_quad(__zkllvm_field_pallas_base a) {

__zkllvm_field_pallas_base res = 1;
for (int i = 0; i < 4; ++i) {
res *= a;
}
return res;
}

[[circuit]] __zkllvm_field_pallas_base
field_arithmetic_example(__zkllvm_field_pallas_base a,
__zkllvm_field_pallas_base b) {

__zkllvm_field_pallas_base c = (a + b) * a + b * (a + b) * (a + b);
const __zkllvm_field_pallas_base constant = 0x12345678901234567890_cppui255;
return c * c * c / (b - a) + pow_quad(a) + constant;
}

The same example would look as follows if it called the crypto3 SDK .


#include <nil/crypto3/algebra/curves/pallas.hpp>

using namespace nil::crypto3::algebra::curves;
typename pallas::base_field_type::value_type pow_quad(typename pallas::base_field_type::value_type a) {
typename pallas::base_field_type::value_type res = 1;
for (int i = 0; i < 4; ++i) {
res *= a;
}
return res;
}

[[circuit]] typename pallas::base_field_type::value_type
field_arithmetic_example(typename pallas::base_field_type::value_type a,
typename pallas::base_field_type::value_type b) {

typename pallas::base_field_type::value_type c = (a + b) * a + b * (a + b) * (a + b);
const typename pallas::base_field_type::value_type constant = 0x12345678901234567890_cppui255;
return c * c * c / (b - a) + pow_quad(a) + constant;
}
for loops in Rust

To optimise execution speed, C++ automatically unrolls for loops when creating circuit IRs.

However, this is not the case for Rust. Instead, Rust circuits must use the zkllvm-unwrap crate to unroll loops.

To unwrap loops, add the #[unroll_for_loops] directive before a function that contains them.

Using built-in assigner checks

There are several cases when a circuit needs to be stopped and its proof needs to be rejected.

For example, if a circuit is designed to verify EdDSA signatures, its proof needs to be rejected as soon as these signatures do not match. There is no need to execute the remainder of the circuit.

To enforce such a check, use the __builtin_assigner_exit_check() / std::intrinsics::assigner_exit_check() function. If the condition passed to the function evaluates to false, no code below the function will be executed.


const int A = 800;

[[circuit]] int verify_numbers_and_return_sum(int b, int c) {
bool is_mul_product_equal_to_const = (b * c) == a;
__builtin_assigner_exit_check(is_mul_product_equal_to_const);

return b + c;
}

info

Note that the std::intrinsics::assigner_exit_check() is unsafe in Rust and all calls to it must be done within an unsafe block.

Using built-in functions

zkLLVM also offers built-in hash functions for common cryptography tasks. The below example uses the built-in hash function to produce a hash of two blocks.


#include <nil/crypto3/hash/algorithm/hash.hpp>
#include <nil/crypto3/hash/sha2.hpp>

using namespace nil::crypto3;

[[circuit]] typename hashes::sha2<256>::block_type produce_hash_of_two_blocks(
typename hashes::sha2<256>::block_type first_input_block, typename hashes::sha2<256>::block_type second_input_block) {

typename hashes::sha2<256>::block_type hash_result = hash<hashes::sha2<256>>(first_input_block, second_input_block);

return hash_result;
}

Using built-in bit (de)composition

While circuits usually operate with elliptic curve field elements, there may be cases when a circuit might need to compose/decompose data into/from bits. This can be easily done by calling the corresponding built-in functions.

info

Built-in bit composition and decomposition are currently only available for C++.

Bit composition

info

For improved efficiency, bit composition still 'packs' bits in curve field elements.

The following example composes a Pallas curve field element into bits and returns the result.

The is_msb boolean determines whether the bits are composed using the MSB (most significant bit) order.


#include <nil/crypto3/algebra/curves/pallas.hpp>

using namespace nil::crypto3::algebra::curves;

constexpr bool is_msb = true;

[[circuit]] typename pallas::base_field_type::value_type compose(
std::array<typename pallas::base_field_type::value_type, 128> input) {

return __builtin_assigner_bit_composition(input.data(), 128, is_msb);
}

Bit decomposition

The below example decomposes bits into Pallas base field type elements.

#include <nil/crypto3/algebra/curves/pallas.hpp>

using namespace nil::crypto3::algebra::curves;

constexpr bool is_msb = true;
constexpr std::size_t bits_amount = 64;

[[circuit]] std::array<typename pallas::base_field_type::value_type, bits_amount>
decompose(uint64_t input) {

std::array<typename pallas::base_field_type::value_type, bits_amount> result;

__builtin_assigner_bit_decomposition(result.data(), bits_amount, input, is_msb);

return result;
}