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 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
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.
- C++
- Rust
__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;
}
#![no_main]
#[unroll_for_loops]
fn pow_quad(a: __zkllvm_field_pallas_base) -> __zkllvm_field_pallas_base {
let mut res: __zkllvm_field_pallas_base = 1g;
for i in 0..4 {
res *= a;
}
res
}
#[circuit]
pub fn field_arithmetic_example(
a: __zkllvm_field_pallas_base,
b: __zkllvm_field_pallas_base,
) -> __zkllvm_field_pallas_base {
let c = (a + b) * a + b * (a + b) * (a + b);
const CONSTANT: __zkllvm_field_pallas_base = 0x12345678901234567890g;
let result = c * c * c / (b - a) + pow_quad(a) + CONSTANT;
result
}
The same example would look as follows if it called the crypto3
SDK .
- C++
- Rust
#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;
}
#![no_main]
#![feature(const_trait_impl)]
#![feature(effects)]
use ark_ff::{Field, MontFp};
use ark_pallas::Fq;
#[unroll_for_loops]
pub fn pow_quad(a: Fq) -> Fq {
let mut res: Fq = Fq::ONE;
for i in 0..4 {
res *= a;
}
res
}
#[circuit]
pub fn field_arithmetic_example(a: Fq, b: Fq) -> Fq {
let c = (a + b) * a + b * (a + b) * (a + b);
const CONSTANT: Fq = MontFp!("0x12345678901234567890");
c * c * c / (b - a) + pow_quad(a) + CONSTANT
}
for
loops in RustTo 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.
- C++
- Rust
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;
}
#![no_main]
const A: i32 = 800;
#[circuit]
pub fn verify_numbers_and_return_sum(b: i32, c: i32) -> i32 {
let is_mul_product_equal_to_const: bool = (b * c) == a;
unsafe {
std::intrinsics::assigner_exit_check(is_mul_product_equal_to_const);
}
b + c
}
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.
- C++
- Rust
#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;
}
#![no_main]
use std::intrinsics::assigner_sha2_256;
use ark_pallas::Fq;
type BlockType = [Fq; 2];
#[circuit]
pub fn produce_hash_of_two_blocks(
first_input_block: BlockType,
second_input_block: BlockType) -> BlockType {
let hash_result = assigner_sha2_256(
[first_input_block[0].0, first_input_block[1].0],
[second_input_block[0].0, second_input_block[1].0],
);
[hash_result[0].into(), hash_result[1].into()]
}
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.
Built-in bit composition and decomposition are currently only available for C++.
Bit composition
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;
}