Foreword

Recent rapid advances in R1CS-based SNARKs finally make application of zero-knowledge proofs practical for bringing scalability and privacy to blockchains.

At the same time, most existing languages and frameworks for constructing R1CS circuits, while being useful in academic research and prototyping, do not provide a satisfying degree of expressiveness and brevity to write readable and easily maintainable programs. A notable exception is xJsnark, but being based on Java it lacks a lot of safety features of modern functional languages.

Further, existing frameworks completely lack functionality specific for smart contracts. Security and safety aspects are crucial for developing smart contracts since they deal with valuable financial assets. Modern smart contract languages, such as Simplicity or Libra's Move, deliberately made design choices that favor safety and formal verifiability of the code over generalistic expressiveness.

Zinc was created to fill the gap between these two worlds: provide a smart contract language optimized for R1CS circuits, which is reliable and simple at the same time, and can be quickly learned by a large number of software developers.

We decided to borrow the Rust syntax and semantics. Zinc is a subset of Rust with minor differences dictated by the subtleties of R1CS circuits. It is easily learnable by any developer familiar with Rust, Golang, C++ or other C-like languages. Also, experience with Solidity will help in understanding some smart contract specifics.

The language is under heavy development, so many of its aspects will eventually be improved or changed. However, the basic principles, such as security and simplicity, will never be questioned.

Design background

The goal of Zinc is to make writing safe zero-knowledge programs and ZKP-based smart contracts easy. It has been designed with the following principles in mind:

  • Security. It should be easy to write deterministic and secure programs. Conversely, it should be hard to write code to exploit some possible vulnerabilities found in other programming languages.
  • Safety. The language must enforce the most strict semantics available, such as a strong static explicit type system.
  • Efficiency. The code should compile to the most efficient circuit possible.
  • Cost-exposition. Performance costs that cannot be optimized efficiently must be made explicit to the developers. An example is the requirement to explicitly specify the loop range with constants.
  • Simplicity. Anyone familiar with C-like languages (Javascript, Java, Golang, C++, Rust, Solidity, Move) should be able to learn Zinc quickly and with minimum effort.
  • Readability. The code in Zinc should be easily readable to anybody familiar with the C++ language family. There should be no counter-intuitive concepts.
  • Minimalism. Less code is better. There should ideally be only one way to do something efficiently. Complexity should be reduced.
  • Expressiveness. The language should be powerful enough to make building complex programs easy.
  • Turing incompleteness. Unbounded looping and recursion are not permitted in Zinc. This not only allows more efficient R1CS circuit construction but also makes formal verifiability about the call and stack safety easier and eliminates the gas computation problem inherent to Turing-complete smart contract platforms, such as EVM.

Key features

  • Type safety
  • Type inference
  • Immutability
  • Movable resources as a first-class citizen
  • Module definition and import
  • Expressive syntax
  • Industrial-grade compiler optimizations
  • Turing incompleteness: no recursion or unbounded looping
  • Flat learning curve for Rust/JS/Solidity/C++ developers

Comparison to Rust

Zinc is designed specifically for ZK-circuits and ZKP-based smart contract development, so some differences from Rust are inevitable.

Type system

We need to adapt the type system to be efficiently representable in finite fields, which are the basic building block of R1CS. The current type system mostly follows Rust, but some aspects are borrowed from smart contract languages. For example, Zinc provides integer types with 1-byte step sizes, like those in Solidity.

Ownership and borrowing

Memory management is very different in R1CS circuits compared to the von Neumann architecture. Also, since R1CS does not imply parallel programming patterns, a lot of elements of the Rust design would be unnecessary and redundant. Zinc has no ownership mechanism found in Rust because all variables will be passed by value. The borrowing mechanism is still being designed, but probably, only immutable references will be allowed shortly.

Loops and recursion

Zinc is a Turing-incomplete language, as it does not allow recursion and variable loop indexes. Every loop range must be bounded with constant literals or expressions.

First circuit

Zinc installation

To start using the Zinc framework, do the following:

  1. Download its binaries for your OS and architecture.
  2. Add the folder with the binaries to PATH
  3. Use the binaries via your favorite terminal

The Zinc framework consists of three tools:

  • zargo circuit manager
  • znc Zinc compiler
  • zvm Zinc virtual machine

zargo can use the compiler and virtual machine through its interface, so you will only need zargo to work with your circuits.

For more information on zargo, check out this chapter.

Let's now move on to writing 'Hello, World!' in Zinc!

The Visual Studio Code extension

There is a syntax highlighting extension for Zinc called Zinc Syntax Highligthing. The IDE should recommend installing it once you have opened a Zinc file!

Creating the circuit

Let's create our first circuit, which will be able to prove knowledge of some sha256 hash preimage:

zargo new preimage
cd preimage

The command above will create a directory with Zargo.toml manifest and the src/ folder with an entry point module main.zn.

Let's replace the main.zn contents with the following code:

use std::crypto::sha256;
use std::convert::to_bits;
use std::array::pad;

const FIELD_SIZE: u64 = 254;
const FIELD_SIZE_PADDED: u64 = FIELD_SIZE + 2 as u64;
const SHA256_HASH_SIZE: u64 = 256;

fn main(preimage: field) -> [bool; SHA256_HASH_SIZE] {
    let preimage_bits: [bool; FIELD_SIZE] = to_bits(preimage);
    let preimage_bits_padded: [bool; FIELD_SIZE_PADDED] = pad(preimage_bits, 256, false);
    sha256(preimage_bits_padded)
}

All-in-one command

When you have finished writing the code, run zargo proof-check. This command will build and run the circuit, generate keys for trusted setup, generate a proof and verify it.

Step by step

Let's get through each step of the command above manually to better understand what is under the hood. Before you start, run zargo clean to remove all the build artifacts.

Building the circuit

Now, you need to compile the circuit into Zinc bytecode:

zargo build

The command above will write the bytecode to the build directory located in the project root. There is also a file called witness.json in the build directory, which is used to provide the secret witness data to the circuit.

Running the circuit

Before you run the circuit, open the data/witness.json file with your favorite editor and fill it with some meaningful values.

Now, execute zargo run > data/public-data.json to run the circuit and write the resulting public data to a file.

There is a useful tool called jq. You may use it together with zargo run to highlight, edit, filter the output data before writing it to the file: zargo run | jq > data/public-data.json.

For more information on jq, visit the official manual.

Trusted setup

To be able to verify proofs, you must create a pair of keys for the prover and the verifier.

To generate a new pair of proving and verifying keys, use this command:

zargo setup

Generating a proof

To generate a proof, provide the witness and public data to the Zinc VM with the following command:

zargo prove > proof.txt

This will also write the program's output to data/public-data.json which is later used by the verifier.

Verifying a proof

Before verifying a proof, make sure that the prover and verifier use the same version of the Zinc framework. Some versions may be compatible, but it is to be decided yet.

To verify a proof, pass it to the Zinc VM with the same public data you used to generated it, and the verification key:

zargo verify < proof.txt

Congratulations! You have developed your first circuit and verified your first Zero-Knowledge Proof!

Feel free to proceed to the next chapters to know more about the Zinc framework!

Merkle proof

In this chapter, we will implement a circuit able to validate the Merkle tree root hash.

At this stage of reading the book, you may be unfamiliar with some language concepts. So, if you struggle to understand some examples, you are welcome to read the rest of the book first, and then come back here.

Our circuit will accept the tree node path, address, and the balance available as the secret witness data. The public data will be the Merkle tree root hash.

Creating a new project

Let's create a new circuit called merkle-proof:

zargo new merkle-proof
cd merkle-proof

Now, you can open the project in your favorite IDE and go to src/main.zn, where we are going to start writing the circuit code.

Defining types

Let's start by defining the secret witness data arguments and the public data return type.

struct PublicInput {
    root_hash: [bool; 256],
}

fn main(
    address: [bool; 10], // the node address in the merkle tree
    balance: field, // the balance stored in the node
    merkle_path: [[bool; 256]; 10] // the hash path to the node
) -> PublicInput {
    // ...
}

As you can see, some complex types are used in several places of our code, so it is very convenient to create an alias for such type.


# #![allow(unused_variables)]
#fn main() {
type Sha256Digest = [bool; 256];
#}

Creating functions

Now, we will write a function to calculate the sha256 hash of our balance. We need it to verify the balance stored within the leaf node at our Merkle tree path.


# #![allow(unused_variables)]
#fn main() {
fn balance_hash(balance: field) -> Sha256Digest {
    let bits = std::convert::to_bits(balance); // [bool; 254]
    let bits_padded = std::array::pad(bits, 256, false); // [bool; 256]
    std::crypto::sha256(bits_padded) // [bool; 256] a.k.a. Sha256Digest
}
#}

The function accepts balance we passed as secret witness data, converts it into a bit array of length 254 (elliptic curve field length), and pads the array with 2 extra zero bits, since we are going to pass 256 bit vector to the sha256 function.

We have also used here three functions from the Zinc standard library from three different modules. The std::crypto::sha256-like paths might seem a bit verbose, but we will solve this problem later.

At this stage, this is how our code looks like:

type Sha256Digest = [bool; 256];

struct PublicInput {
    root_hash: Sha256Digest,
}

fn balance_hash(balance: field) -> Sha256Digest {
    let bits = std::convert::to_bits(balance); // [bool; 254]
    let bits_padded = std::array::pad(bits, 256, false); // [bool; 256]
    std::crypto::sha256(bits_padded) // [bool; 256] a.k.a. Sha256Digest
}

fn main(
    address: [bool; 10], // the node address in the merkle tree
    balance: field, // the balance stored in the node
    merkle_path: [Sha256Digest; 10] // the hash path to the node
) -> PublicInput {
    let leaf_hash = balance_hash(balance);

    // ...
}

Now, we need a function to calculate a tree node hash:


# #![allow(unused_variables)]
#fn main() {
fn merkle_node_hash(left: Sha256Digest, right: Sha256Digest) -> Sha256Digest {
    let mut data = [false; 512]; // [bool; 512]

    // Casting to u16 is needed to make the range types equal,
    // since 0 will be inferred as u8, and 256 - as u16.
    for i in 0 as u16..256 {
        data[i] = left[i];
        data[256 + i] = right[i];
    }

    std::crypto::sha256(data) // [bool; 256] a.k.a. Sha256Digest
}
#}

The Zinc standard library does not support array concatenation yet, so, for now, we will do it by hand, allocating an array to contain two leaf node digests, then put the digests together and hash them with std::crypto::sha256.

Finally, let's define a function to calculate the hash of the whole tree:


# #![allow(unused_variables)]
#fn main() {
fn restore_root_hash(
    leaf_hash: Sha256Digest,
    address: [bool; 10],
    merkle_path: [Sha256Digest; 10],
) -> Sha256Digest
{
    let mut current = leaf_hash; // Sha256Digest

    // Traverse the tree from the left node to the root node
    for i in 0..10 {
        // Multiple variables binding is not supported yet,
        // so we going to store leaves as an array of two digests.
        // If address[i] is 0, we are in the left node, otherwise,
        // we are in the right node.
        let left_and_right = if address[i] {
            [current, merkle_path[i]] // [Sha256Digest; 2]
        } else {
            [merkle_path[i], current] // [Sha256Digest; 2]
        };

        // remember the current node hash
        current = merkle_node_hash(left_and_right[0], left_and_right[1]);
    }

    // return the root node hash
    current
}
#}

Congratulations! Now we have a working circuit able to verify the Merkle proof!

// main.zn

type Sha256Digest = [bool; 256];

fn balance_hash(balance: field) -> Sha256Digest {
    let bits = std::convert::to_bits(balance); // [bool; 254]
    let bits_padded = std::array::pad(bits, 256, false); // [bool; 256]
    std::crypto::sha256(bits_padded) // [bool; 256] a.k.a. Sha256Digest
}

fn merkle_node_hash(left: Sha256Digest, right: Sha256Digest) -> Sha256Digest {
    let mut data = [false; 512]; // [bool; 512]

    // Casting to u16 is needed to make the range types equal,
    // since 0 will be inferred as u8, and 256 - as u16.
    for i in 0 as u16..256 {
        data[i] = left[i];
        data[256 + i] = right[i];
    }

    std::crypto::sha256(data) // [bool; 256] a.k.a. Sha256Digest
}

fn restore_root_hash(
    leaf_hash: Sha256Digest,
    address: [bool; 10],
    merkle_path: [Sha256Digest; 10],
) -> Sha256Digest
{
    let mut current = leaf_hash; // Sha256Digest

    // Traverse the tree from the left node to the root node
    for i in 0..10 {
        // Multiple variables binding is not supported yet,
        // so we going to store leaves as an array of two digests.
        // If address[i] is 0, we are in the left node, otherwise,
        // we are in the right node.
        let left_and_right = if address[i] {
            [current, merkle_path[i]] // [Sha256Digest; 2]
        } else {
            [merkle_path[i], current] // [Sha256Digest; 2]
        };

        // remember the current node hash
        current = merkle_node_hash(left_and_right[0], left_and_right[1]);
    }

    // return the root node hash
    current
}

struct PublicInput {
    root_hash: Sha256Digest,
}

fn main(
    address: [bool; 10],
    balance: field,
    merkle_path: [Sha256Digest; 10]
) -> PublicInput {
    let leaf_hash = balance_hash(balance);

    let root_hash = restore_root_hash(
        leaf_hash,
        address,
        merkle_path,
    );

    PublicInput {
        root_hash: root_hash,
    }
}

Defining a module

Our main.zn module has got a little overpopulated by now, so let's move our functions to another one called merkle. At first, create a file called merkle.zn in the src directory besides main.zn. Then, move everything above the PublicInput definition to that file. Our main.zn will now look like this:

struct PublicInput {
    root_hash: Sha256Digest, // undeclared `Sha256Digest`
}

fn main(
    address: [bool; 10],
    balance: field,
    merkle_path: [Sha256Digest; 10] // undeclared `Sha256Digest`
) -> PublicInput {
    let leaf_hash = balance_hash(balance); // undeclared `balance_hash`

    let root_hash = restore_root_hash( // undeclared `restore_root_hash`
        leaf_hash,
        address,
        merkle_path,
    );

    PublicInput {
        root_hash: root_hash,
    }
}

This code will not compile, as we have several items undeclared now! Let's define our merkle module and resolve the function paths:

mod merkle; // defined a module

struct PublicInput {
    root_hash: merkle::Sha256Digest, // use a type declaration from `merkle`
}

fn main(
    address: [bool; 10],
    balance: field,
    merkle_path: [merkle::Sha256Digest; 10] // use a type declaration from `merkle`
) -> PublicInput {
    let leaf_hash = merkle::balance_hash(balance); // call a function from `merkle`

    // call a function from `merkle`
    let root_hash = merkle::restore_root_hash(
        leaf_hash,
        address,
        merkle_path,
    );

    PublicInput {
        root_hash: root_hash,
    }
}

Perfect! Now all our functions and types are defined. By the way, let's have a glance at our merkle module, where you can find another improvement!


# #![allow(unused_variables)]
#fn main() {
use std::crypto::sha256; // an import

type Sha256Digest = [bool; 256];

fn balance_hash(balance: field) -> Sha256Digest {
    let bits = std::convert::to_bits(balance);
    let bits_padded = std::array::pad(bits, 256, false);
    sha256(bits_padded)
}

fn merkle_node_hash(left: Sha256Digest, right: Sha256Digest) -> Sha256Digest {
    let mut data = [false; 512];

    for i in 0 as u16..256 {
        data[i] = left[i];
        data[256 + i] = right[i];
    }

    sha256(data)
}

fn restore_root_hash(
    leaf_hash: Sha256Digest,
    address: [bool; 10],
    merkle_path: [Sha256Digest; 10],
) -> Sha256Digest
{
    let mut current = leaf_hash;

    for i in 0..10 {
        let left_and_right = if address[i] {
            [current, merkle_path[i]]
        } else {
            [merkle_path[i], current]
        };

        current = merkle_node_hash(left_and_right[0], left_and_right[1]);
    }

    current
}
#}

You may notice a use statement at the first line of code. It is an import statement which is designed to prevent using long repeated paths in our code. As you can see, now we call the standard library function like this sha256(data), but not like that std::crypto::sha256(data).

Finalizing

Congratulations, you are an experienced Zinc developer! Now, you may build the circuit, generate and verify a proof, like it was explained in the previous chapter, and move on to reading the rest of the book!

Basic concepts

A Zinc project consists of an entry point file called main.zn and zero or more module files whose contents can be imported into the main file.

The entry point file must contain the main function, which accepts secret witness data and returns public input data. For more detail, see the next section.

Module files may contain only declarations of types, functions, and constants.

Examples

Entry point file

/// 
/// 'src/main.zn'
///
/// Proves a knowledge of a cube root `r` for a given public input `x`.
///

mod simple_math;

use simple_math::cube;

fn main(x: field, r: field) -> field {
    assert!(x == cube(r), "x == r ^ 3");
    x
}

Module simple_math file

/// 
/// 'src/simple_math.zn'
/// 

/// Returns x^3.
fn cube(x: field) -> field {
    x * x * x
}

Input and output

In terms of zero-knowledge circuits, the information that we are trying to prove valid is called public input. And the secret piece of information that may be known only by prover is called witness.

In the Zinc framework, the program's result becomes public input. That means that whatever the main function returns should be known by verifier. All other runtime values including arguments represent circuit's witness.

So when verifier checks the program's result and the proof it is safe to state that:

There is some set of arguments known to prover, which, being provided into program yields the same output.

The prover must provide program's arguments to generate the result and proof.

Verifier will use the proof to check that the result has been obtained by executing the program.

The following example illustrates a circuit proving knowledge of some sha256 hash preimage:

use std::crypto::sha256;

fn main(preimage: [bool; 256]) -> [bool; 256] {
    sha256(preimage)
}

Built-in functions

There are several built-in functions, which can be called directly from anywhere in your code.

assert!()

This function creates a custom constraint in any place of your code. Using assert!() you can check whether some condition is true and make the circuit exit with an error if otherwise:

const BAD_VALUE: u8 = 42;

fn wrong(a: u8, b: u8) -> u8 {
    let c = a + b - BAD_VALUE;
    assert!(a + b == c, "always fails");
    c
}

dbg!()

This function prints data to the terminal and is used only for debugging purposes.

The first argument is the format string, where each {} placeholder is replaced with a corresponding value from the rest of the arguments. The number of placeholders must be equal to the number of the arguments not including the format string.

// a = 5, b = 3
fn print_sum(a: u8, b: u8) {
    dbg!("{} + {} = {}", a, b, a + b); // prints '5 + 3 = 8'
}

Example

To call such a function, use the <identifier>!(arg1, arg2, ...) syntax, as in the following example:

fn main(/* ... */) {
    let value: u8 = 42;
    dbg!("{}", value);
    assert!(value == 42);
}

If you are familiar with Rust, it can resemble the macro syntax found there, but actually, these functions have nothing to do with macros. Instead, they represent some special Zinc VM instructions.

The exhaustive list of function signatures is provided in Appendix D.

Standard library

The standard library is currently located in a built-in module called std. The library contains three modules for now:

  • crypto - cryptographic and hash functions
    • ecc - elliptic curve cryptography
    • schnorr - EDDSA signatyre verification
  • convert - bit array conversion functions
  • array - array processing functions
  • ff - finite field functions

All the function signatures are listed in Appendix E.

Standard library items can be used directly or be imported with use:

use std::crypto::sha256;

fn main(preimage: [bool; 256]) -> ([bool; 256], (field, field)) {
    let input_sha256 = sha256(preimage); // through import
    dbg!(input_sha256);

    let input_pedersen = std::crypto::pedersen(preimage); // directly
    dbg!(input_pedersen);

    (input_sha256, input_pedersen)
}

Variables and types

This chapter describes the Zinc language concepts. Here you will learn about variables, types, and functions.

Variables

As it was said before, Zinc is mostly about safety and security. Thus, variables are immutable by default. If you are going to change their values, you must explicitly mark them as mutable. It protects your data from accidental mutating where the compiler is unable to check your intentions.

fn test() {
    let x = 0;
    // compile error: mutating an immutable variable
    // x = 42;

    let mut y = 0;
    y = 42; // ok
}

If you are familiar with Rust, you will not have any trouble understanding this concept, since the syntax and semantics are almost identical. However, pattern matching and destructuring are not implemented yet.

Immutable variables are similar to constants. Like with constants, you cannot change the immutable variable value. However, constants cannot infer their type and you must specify it explicitly.

In contrast to Rust, variables can only be declared in functions. If you need a global variable, you should declare a constant instead. This limitation is devised to prevent unwanted side effects, polluting the global namespace, and bad design.

const VALUE: field = 0;

fn test() {
    let variable = VALUE;
}

Variable shadowing can be a convenient feature, but Zinc is going to enforce warning-as-error development workflow, forbidding shadowing as a potentially unsafe trick. You should use mutable variables and type suffixes if you want several variables with similar logical meaning.

fn test() {
    let mut x = 5;
    {
        // compile error: redeclared variable 'x'
        // let x = 25;
    }
    // compile error: redeclared variable 'x'
    // let x = 25;

    x = 25; // ok
}

Types

Zinc is a statically typed language, thus all the variables must have a type known at the compile time. Strict type system allows to catch the majority of runtime errors, which are very common to dynamically typed languages. Zinc type system closely resembles that of Rust, but with some modifications, limitations, and restrictions.

Types are divided into several groups:

To read more about casting, conversions, and type policy, go to this chapter.

You can declare type aliases in Zinc, which allow you to shorten type signatures of complex types by giving them a name:

type ComplexType = [(u8, [bool; 8], field); 16];

fn example(data: ComplexType) {}

Scalar types

Scalar types are also called primitive types and contain a single value.

Unit

The unit type and value are described with empty round parenthesis () and have no differences from the same type in Rust. Values of that type are implicitly returned from functions, blocks, and other expressions which do not return a value explicitly. Also, this type can be used as a placeholder for input, witness and output types of the main function.

() is the literal for both unit type and value. The unit type values cannot be used by any operators or casted back and forth.

The unit type can exist as a standalone value:

let x = (); // ()

It is implicitly returned by blocks or functions:

fn check(value: bool) {
    // several statements
};

let y = check(true); // y is ()

Boolean

bool is the boolean type keyword.

Boolean value is represented as field with value set to either 0 or 1. To ensure type safety casting between boolean and integer types is not allowed. In general, its behavior is indistinguishable from the same type from Rust or other C-like languages.

Literals

true and false.

Examples

let a = true;
let b: bool = false;

if a && !b {
    debug(a ^^ b);
};

Integer

Integer types are somewhat different from those of Rust since they are extended to be able to use any size between 1 and 32 bytes. This feature was borrowed from Solidity and it helps to reduce the number of constraints and smart contract size. Internal integer representation uses the BN256 field of different bitlength.

Types

  • u8 .. u248: unsigned integers
  • i8 .. i248: signed integers
  • field: the native field integer

Integer types bitlength step equals 8, that is, only the following bitlengths are possible: 8, 16, ..., 240, 248.

A field value is a native field element of the elliptic curve used in the constraint system. It represents an unsigned integer of bitlength equal to the field modulus length (e.g. for BN256 the field modulus length is 254 bit).

All the types are represented using field as their basic building block. When an integer variable is allocated, its bitlength must be enforced in the constraint system.

Literals

  • decimal: 0, 1, 122, 574839572494237242
  • hexadecimal: 0x0, 0xfa, 0x0001, 0x1fffDEADffffffffffBEEFffff

Following the Rust rules, only unsigned integer literals can be expressed, since the unary minus is not a part of the literal but a standalone operator. Thus, unsigned values can be implicitly casted to signed ones using the unary minus.

Casting

Casting is possible only to a type with greater bitlength. Probably, this behavior will become less strict in the future.

Inference

If the literal type is not specified, the minimal possible bitlength is inferred.

Examples

let a = 0; // u8
let a: i24 = 0; // i24
let b = 256; // u16
let c = -1;  // i8
let c = -129; // i16
let d = 0xff as field; // field
let e: field = 0; // field

Arrays

Arrays are collections of values of the same type sequentially stored in the memory.

Fixed-sized arrays follow the Rust rules. The only exception is the restriction to constant indexes, that is, you cannot index an array with anything but a constant expression for now.

Arrays support the index and slice operators, which is explained in detail here.

let mut fibbonaci = [0, 1, 1, 2, 3, 5, 8, 13];
let element = fibbonaci[3];
fibbonaci[2] = 1;

Tuples

Tuples are anonymous collections of values of different types, sequentially stored in memory and gathered together due to some logical relations.

Like in Rust, () is a void value, (value) is a parenthesized expression, and (value,) is a tuple with one element.

Tuple fields can be accessed via the dot operator, which is explained in detail here.

let mut tuple: (u8, field) = (0xff, 0 as field);
tuple.0 = 42;
dbg!("{}", tuple.1);

Structures

The structure is a custom data type that lets you name and package together multiple related values that make up a meaningful group. Structures allow you to easily build complex data types and pass them around your code with as little verbosity as possible.

Structure fields can be accessed via the dot operator, which is explained in detail here.

struct Person {
    age: u8,
    id: u64,
}

let mut person = Person {
    age: 24,
    id: 123456789 as u64,
};
person.age = 25;

Enumerations

Enums allow you to define a type by enumerating its possible values. Only simple C-like enums are supported for now, which are groups of constants, following the Rust syntax:

enum Order {
    FIRST = 0,
    SECOND = 1,
}

Enum values can be used with match expressions to define the behavior in every possible case:

let value = Order::FIRST;
let result = match value {
    Order::FIRST => do_this(),
    Order::SECOND => do_that(),
};

The enum values can be implicitly casted to unsigned integers of enough bitlength using let statements or explicitly using the as operator:

let x = Order::FIRST; // the type is Order (inference)
let y: u8 = Order::SECOND; // the type is u8 (implicit casting)
let z = Order::SECOND as u8; // the type is u8 (explicit casting)

Strings

For now, strings have very limited implementation and usability.

The string type exists only in the literal form and can only appear in dbg and assert built-in functions:

dbg!("{}", 42);

assert!(true != false, "a very obvious fact");

Casting and conversions

The language enforces static strong explicit type semantics. It is the most strict type system available since reliability is above everything. However, some inference abilities will not do any harm, so you do not have to specify types in places where they are highly obvious.

Explicit

Type conversions can be only performed on the integer and enumeration types with the casting operator. This chapter explains the operator's behavior in detail.

Implicit

The let statement can perform implicit type casting of integers if the type is specified to the left of the assignment symbol. Let us examine the statement:

let a: field = 42 as u32;
  1. 42 is inferred as a value of type u8.
  2. 42 is cast from u8 to u32.
  3. The expression 42 as u32 result is cast to field.
  4. The field value is assigned to the variable a.

The second case of implicit casting is the negation operator, which always returns a signed integer type value of the same bitlength, regardless of the input argument.

let positive = 100; // u8
let negative = -positive; // i8

This chapter describes the negation operator with more detail.

Inference

For now, Zinc infers types in two cases: integer literals and let bindings.

Integer literals are always inferred as values of the minimal possible size. That is, 255 is a u8 value, whereas 256 is a u16 value. Signed integers must be implicitly cast using the negation operator.

The let statement can infer types in case its type is not specified.

let value = 0xffffffff_ffffffff_ffffffff_ffffffff;

In the example above, the value variable gets type u128, since 128 bytes are enough to represent the value 0xffffffff_ffffffff_ffffffff_ffffffff;

Function

The function is the only callable type in Zinc and it closely follows the Rust syntax. However, R1CS specifics require that functions must be executed completely, thus there is no return statement in Zinc. The only way to return a value is to specify it as the last unterminated statement of the function block.

Functions consist of several parts: the name, arguments, return type, and the code block. The function name uniquely defines the function within its namespace. The arguments can be only passed by value, and the function result can only be returned by value. If the return type is omitted, the function is considered to return a void value (). The code block can access the global scope, but it has no information about where the function has been called from.

const GLOBAL: u8 = 31;

fn wierd_sum(a: u8, b: u8) -> u8 {
    side_effect(); // a statement
    a + b + GLOBAL // return value
}

let result = wierd_sum(42, 27);
assert!(result == 100, "the weird sum is incorrect");

Operators

Operators of the Zinc language can be divided into several groups:

#№ Precedence

The top one is executed first.

OperatorAssociativity
::left to right
[] .left to right
- ~ !unary
asleft to right
* / %left to right
+ -left to right
<< >>left to right
&left to right
^left to right
left to right
== != <= >= < >require parentheses
&&left to right
^^left to right
⎮⎮left to right
.. ..=require parentheses
= += -= *= /= %= ⎮= ^= &= <<= >>=require parentheses

Arithmetic operators

Arithmetic operators do not perform any kind of overflow checking at compile time. If an overflow happens, the Zinc VM will fail at runtime.

When it comes to the division of negative numbers, Zinc follows the Euclidean division concept. It means that -45 % 7 == 4. To get the detailed explanation and some examples, see the article.

The +=, -=, *=, /=, %= shortcut operators perform the operation and assign the result to the first operand. The first operand must be a mutable memory location like a variable, array element, or structure field.

Addition

+ and += are binary operators.

Accepts

  1. Integer expression
  2. Expression of the operand 1 type

Returns an integer result of the same type.

Subtraction

- and -= are binary operators.

Accepts

  1. Integer expression
  2. Expression of the operand 1 type

Returns an integer result of the same type.

Multiplication

* and *= are binary operators.

Accepts

  1. Integer expression
  2. Expression of the operand 1 type

Returns an integer result of the same type.

Division

/ and /= are binary operators.

Accepts

  1. Integer expression (any type except field)
  2. Expression of the operand 1 type

Returns an integer result of the same type.

Remainder

% and %= are binary operators.

Accepts

  1. Integer expression (any type except field)
  2. Expression of the operand 1 type

Returns an integer result of the same type.

Negation

- is an unary operator.

Accepts

  1. Integer expression (any type except field)

Returns a signed integer with the same bitlength.

Bitwise operators

Bitwise operators do not perform any kind of overflow checking at compile time. If an overflow happens, the Zinc VM will fail at runtime.

The |=, ^=, &=, <<=, >>= shortcut operators perform the operation and assign the result to the first operand. The first operand must be a mutable memory location like a variable, array element, or structure field.

For now, bitwise operators are allowed for constants only. Witness data will be covered soon.

Bitwise OR

| and |= are binary operators.

Accepts

  1. Integer expression (any type except field)
  2. Expression of the operand 1 type

Returns an integer result of the same type.

Bitwise XOR

^ and ^= are binary operators.

Accepts

  1. Integer expression (any type except field)
  2. Expression of the operand 1 type

Returns an integer result of the same type.

Bitwise AND

& and &= are binary operators.

Accepts

  1. Integer expression (any type except field)
  2. Expression of the operand 1 type

Returns an integer result of the same type.

Bitwise shift left

<< and <<= are binary operators.

Accepts

  1. Integer expression (any type except field)
  2. Constant integer expression

Returns an integer result of the operand 1 type.

Bitwise shift right

>> and >>= are binary operators.

Accepts

  1. Integer expression (any type except field)
  2. Constant integer expression

Returns an integer result of the operand 1 type.

Bitwise NOT

~ is an unary operator.

Accepts

  1. Integer expression (any type except field)

Returns an integer result.

Comparison operators

Equality

== is a binary operator.

Accepts

  1. Integer or boolean expression
  2. Expression of the operand 1 type

Returns the boolean result.

Non-equality

!= is a binary operator.

Accepts

  1. Integer or boolean expression
  2. Expression of the operand 1 type

Returns the boolean result.

Lesser or equals

<= is a binary operator.

Accepts

  1. Integer expression
  2. Expression of the operand 1 type

Returns the boolean result.

Greater or equals

>= is a binary operator.

Accepts

  1. Integer expression
  2. Expression of the operand 1 type

Returns the boolean result.

Lesser

< is a binary operator.

Accepts

  1. Integer expression
  2. Expression of the operand 1 type

Returns the boolean result.

Greater

> is a binary operator.

Accepts

  1. Integer expression
  2. Expression of the operand 1 type

Returns the boolean result.

Logical operators

OR

|| is a binary operator.

Accepts

  1. Boolean expression
  2. Boolean expression

Returns the boolean result.

XOR

^^ is a binary operator.

Accepts

  1. Boolean expression
  2. Boolean expression

Returns the boolean result.

AND

&& is a binary operator.

Accepts

  1. Boolean expression
  2. Boolean expression

Returns the boolean result.

NOT

! is an unary operator.

Accepts

  1. Boolean expression

Returns the boolean result.

Casting operator

as is a binary operator.

Accepts

  1. Expression of any type
  2. Expression of the same or integer type

Returns the casted value.

Casting allowed:

  • from integer to integer
  • from enum to integer
  • to the same type (no effect, no errors)
enum Order {
    First = 1,
}

let a = 1; // inferred as u8
let b = a as i8; // explicit casting to the opposite sign
let c: u8 = Order::First; // implicit casting to an integer

Access operators

Path resolution

:: is a binary operator.

Accepts

  1. Namespace identifier (module, structure, enumeration)
  2. Item identifier (module, type, variable, constant etc.)

Returns the second operand.

Array indexing

[] is a binary operator.

Accepts

  1. Array expression
  2. Integer or range expression

Returns an array element (if the 2nd operand is an integer) or a sub-array (if the 2nd operand is a range).

Field access

. is a binary operator.

Accepts

  1. Tuple or structure expression
  2. Tuple index or structure field name

Returns a tuple or structure element.

Range operators

Range

.. is a binary operator.

Range operator is used only for loop bounds or array slicing.

The operator can accept operands of different integer types. The result will be signed if any of the operands if signed, and the bitlength will be enough to contain the greater range bound.

Accepts

  1. Constant integer expression
  2. Expression of the operand 1 type

Returns a temporary range element to be used as a slice or loop range.

Inclusive range

..= is a binary operator.

The same as the above, but the right range bound is inclusive.

Accepts

  1. Constant integer expression
  2. Expression of the operand 1 type

Returns a temporary range element to be used as a slice or loop range.

Assignment operator

= is a binary operator.

Accepts

  1. Place expression (a descriptor of a memory place, e.g. a variable or array element)
  2. Value expression

Returns ().

Expressions

Expressions consist of operands and operators.

Operators have already been described in this chapter.

Operands

Any syntax constructions computed into values can be used in expressions. Zinc does all the type checking at compile-time, so you can build expressions of arbitrary complexity without caring about type safety. However, you should care about readability and maintainability, since there are probably other people going to work with your code.

Literals

Simple literal operands are the basic elements of an expression:

  • 42 - integer
  • false - boolean
  • "error" - string
  • u128 - type (in casting clauses like 42 as u128)

There are several complex operands worth mentioning. As you will see from the examples, you can nest these constructions as much as you need, but do not abuse this ability too much.

Array

let array = [
    1,
    2,
    3,
    4,
    5,
    1 + 5,
    { let t = 5; t * t },
];

The inner type and array length are inferred by the compiler.

Tuple

let tuple = (42, true, [1, 2, 3]);

The inner types and the tuple type are inferred by the compiler.

Structure

struct Data {
    value: field,
}

let data = Data {
    value: 0,
};

Blocks

A block expression consists of zero or more statements and an optional result expression. Every block starts a new scope of visibility.

let c = {
    let a = 5;
    let b = 10;
    a + b
};

Conditionals

if

An if conditional expression consists of the condition, main block, and optional else block. Every block starts a new scope of visibility.

let condition = true;
let c = if condition {
    let a = 5;
    a
} else {
    let b = 10;
    b
};

match

The match expression is a syntactic sugar for nested conditional expressions. Each branch block starts a new scope of visibility.

let value = MyEnum::ValueOne;

match value {
    MyEnum::ValueOne => { ... }
    MyEnum::ValueTen => { ... }
    _ => { ... }
}

For now, only the following match patterns are supported:

  • constant (e.g. 42)
  • path (e.g. MyEnum::ValueOne)
  • variable binding (e.g. value)
  • wildcard (_)

Only simple types can be used as the match scrutinee for now, this is, you cannot match an array, tuple or structure.

Statements

The basic element of a Zinc program is a statement.

Statements are divided into several types:

  1. Declaration statements
  2. Expression statements
  3. Control statements

Declaration statements

The declaration statements declare a new item, that is, a type, variable or module.

let variable declaration

let [mut] {identifier}[: {type}] = {expression};

The let declaration behaves just like in Rust, but it does not allow uninitialized variables.

The type is optional and is used mostly to cast integer literal or double-check the expression result type, otherwise, it is inferred.

let mut variable: field = 0;

type alias declaration

type {identifier} = {type};

The type statement declares a type alias to avoid repeating complex types.

type Alias = (field, u8, [field; 8]);

struct type declaration

The struct statement declares a structure.

struct Data {
    a: field,
    b: u8,
    c: (),
}

enum type declaration

The enum statement declares an enumeration.

enum List {
    A = 1,
    B = 2,
    C = 3,
}

fn type declaration

The fn statement declares a function.

fn sum(a: u8, b: u8) -> u8 {
    a + b
}

impl namespace declaration

The impl statement declares a namespace of a structure or enumeration.

struct Data {
    value: field,
}

impl Data {
    fn print(data: Self) {
        dbg!("{}", data.value);
    }
}

mod module declaration

mod {identifier};

The mod statement declares a new module and behaves the same way as in Rust.

use module import

use {path};

The use statement imports an item from another namespace and behaves the same way as in Rust.

Expression statements

Expression

The expression statement is an expression terminated with a ; to ignore its result. The most common use is the assignment to a mutable variable:

let mut a = 0;
a = 42; // an expression statement ignoring the '()' result of the assignment

For more information on expressions, check this chapter.

Semicolons

In contrast with Rust, expression statements in Zinc must be always terminated with ; to get rid of some ambiguities regarding block and conditional expressions. Let us compare the examples of Rust and Zinc to illustrate the problem.

fn blocks() -> i32 {
    {
        get_unit()
    } // a statement, but only because the block result is ()
    {
        get_integer()
    } // a return expression, only because the block result is an integer
}

In the Rust example above, the blocks are completely identical, but their semantic meaning depends on the block return type. Zinc solves this problem by enforcing all expression statements to be explicitly terminated with a semicolon, like in the following Zinc example:

fn blocks() -> i32 {
    {
        get_unit()
    }; // a statement, because it is explicitly terminated with a semicolon
    {
        get_integer()
    } // a return expression, because it goes the last in the function block
}

Conditional and match expressions follow the same rules as simple blocks.

Control statements

Control statements neither ignore the result nor declare a new item. The only such statement is the for-while loop.

for-while loop

for {identifier} in {range} [while {expression}] {
    ...
}

The for loop statement behaves just like in Rust, but it is merged with the while loop, so the optional while condition is checked before each iteration of the loop. The while condition expression has access to the inner scope and can use its variables and the loop iterator.

for i in 0..10 while i % x != 8 {
    // do something
};

Only constant expressions can be used as the bounds of the iterator range. The while condition will not cause an early return, but it will suppress the loop body side effects.

Zinc is a Turing-incomplete language, as it is dictated by R1CS restrictions, so loops always have a fixed number of iterations. On the one hand, the loop counter can be optimized to be treated as a constant, reducing the circuit cost, but on the other hand, you cannot force a loop to return early, increasing the circuit cost.

if and match

The conditional and match expressions can act as control statements, ignoring the returned value. To use them in such role, just terminate the expression with a semicolon:


# #![allow(unused_variables)]
#fn main() {
fn unknown(value: u8) -> u8 {
    match value {
        1 => dbg!("One!"),
        2 => dbg!("Two!"),
        _ => dbg!("Perhaps, three!"),
    };
    42
}}
#}

Virtual machine

Zinc code is compiled into bytecode which can be run by Zinc VM.

Zinc VM is a virtual machine that serves three purposes: executing arbitrary computations, generating zero-knowledge proof of performed computations, and verification of the provided proof without knowing the input data.

Zinc VM is a stack-based virtual machine which is similar to many others like the Python VM. Even though the VM is designed considering specifics and limitations of zero-knowledge computations, bytecode instructions only manipulate data on the stack while all zero-knowledge constraints are automatically applied by the virtual machine.

Zargo circuit manager

Zargo is a circuit managing tool, which can create, build, and use circuits to generate and verify proofs.

Commands overview

All the commands have default values, so you may omit them in normal circumstances. See zargo --help for more detail.

new

Creates a new project directory with Zargo.toml manifest file and src/main.zn circuit entry point module.

init

Initializes a new project in an existing directory, creates missing files.

build

Builds the circuit. The build consists of:

  • the bytecode file
  • secret input JSON template
  • public data JSON template

clean

Removes the build directory.

run

Build and runs the circuit on the Zinc VM, writes the result to the terminal.

setup

Generates parameters for the prover using the circuit bytecode.

prove

Generates the proof using the circuit bytecode, parameters generated with setup, and provided public data.

verify

Verifies the proof using the circuit bytecode, parameters generated with setup, proof generated with prove, and provided public data.

proof-check

Executes the full cycle of proof verification, that is, performs run + setup + prove + verify. Mostly for testing purposes.

Workflow example

Short

# create a new circuit called 'zircuit'
zargo new zircuit
cd zircuit/

# write some code

# run the full verification cycle
zargo proof-check

Long

# create a new circuit called 'zircuit'
zargo new zircuit
cd zircuit/

# write some code

# build the circuit
zargo build

# run the circuit and print the result
zargo run

# generate the prover parameters
zargo setup

# edit the 'build/witness.json' and 'build/public-data.json' files

# generate the proof
zargo prove

# verify the proof
zargo verify

Manifest file

A Zinc circuit is described with the manifest file Zargo.toml with the following structure:

[circuit]
name = "test"
version = "0.1.0"

Schnorr signature tool

The schnorr signature tool can perform the following actions:

  • generate a private key
  • sign a message with a private key
  • extract the public key from a private key

Generating a private key

schnorr gen-key > 'private_key.txt'

Signing a message with a private key

From file:

schnorr sign --key 'private_key.txt' --message 'message.txt'

From stdin:

schnorr sign --key 'private_key.txt' --message -

The JSON output can be used as witness data if you want to pass the signature to a circuit.

Extracting the public key

schnorr pub-key < 'private_key.txt' > 'public_key.txt'

Appendix

The following sections contain reference material you may find useful in your Zinc journey.

Lexical grammar (EBNF)

lexeme = comment | identifier | keyword | literal | symbol | EOF ;

comment = single_line_comment | multi_line_comment ;
single_line_comment = '//', ( ? ANY ? - '\n' | EOF ), '\n' | EOF ;
multi_line_comment = '/*', ( ? ANY ? - '*/' ), '*/' ;

identifier = (
    alpha, { alpha | digit | '_' }
  | '_', alpha, { alpha }
- keyword ) ;

keyword =
    'let'
  | 'mut'
  | 'const'
  | 'type'
  | 'struct'
  | 'enum'
  | 'fn'
  | 'mod'
  | 'use'
  | 'impl'

  | 'for'
  | 'in'
  | 'while'
  | 'if'
  | 'else'
  | 'match'

  | 'bool'
  | 'u8' | 'u16' | 'u24' | 'u32' | 'u40' | 'u48' | 'u56' | 'u64'
  | 'u72' | 'u80' | 'u88' | 'u96' | 'u104' | 'u112' | 'u120' | 'u128'
  | 'u136' | 'u144' | 'u152' | 'u160' | 'u168' | 'u176' | 'u184' | 'u192'
  | 'u200' | 'u208' | 'u216' | 'u224' | 'u232' | 'u240' | 'u248' | 'field'
  | 'i8' | 'i16' | 'i24' | 'i32' | 'i40' | 'i48' | 'i56' | 'i64'
  | 'i72' | 'i80' | 'i88' | 'i96' | 'i104' | 'i112' | 'i120' | 'i128'
  | 'i136' | 'i144' | 'i152' | 'i160' | 'i168' | 'i176' | 'i184' | 'i192'
  | 'i200' | 'i208' | 'i216' | 'i224' | 'i232' | 'i240' | 'i248'

  | 'true'
  | 'false'

  | 'as'

  | 'Self'
;

literal = boolean | integer | string ;
boolean = 'true' | 'false' ;
integer =
    '0'
  | digit - '0', { digit }
  | '0x', hex_digit, { hex_digit }
;
string = '"', { ANY - '"' | '\', ANY }, '"' ;

symbol =
    '('
  | ')'
  | '['
  | ']'
  | '{'
  | '}'
  | '_'
  | '.'
  | ':'
  | ';'
  | ','
  | '='
  | '+'
  | '-'
  | '*'
  | '/'
  | '%'
  | '\'
  | '!'
  | '<'
  | '>'
  | '|'
  | '&'
  | '^'
  | '~'
  | '<<'
  | '>>'
  | '+='
  | '-='
  | '*='
  | '/='
  | '%='
  | '|='
  | '&='
  | '^='
  | '::'
  | '=='
  | '!='
  | '<='
  | '>='  
  | '&&'
  | '^^'
  | '||'
  | '..'
  | '..='
  | '<<='
  | '>>='
  | '=>'
  | '->'
;

alpha =
    'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G'
  | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N'
  | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U'
  | 'V' | 'W' | 'X' | 'Y' | 'Z' 
  | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g'
  | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n'
  | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u'
  | 'v' | 'w' | 'x' | 'y' | 'z'
;

digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' ;

hex_digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
  | 'A' | 'B' | 'C' | 'D' | 'E' | 'F'
  | 'a' | 'b' | 'c' | 'd' | 'e' | 'f'
;

The Zinc alphabet

GroupCharacters
whitespaces\t \n \r
lowercaseA B C D E F G H I J K L M N O P Q R S T U V W X Y Z
uppercasea b c d e f g h i j k l m n o p q r s t u v w x y z
numbers0 1 2 3 4 5 6 7 8 9
symbols+ - * / % < = > ⎮ & ^ _ ! ~ ( ) [ ] { } " , . : ;

Syntax grammar (EBNF)

file = { module_local_statement } ;

(* Statements *)
module_local_statement =
    const_statement
  | type_statement
  | struct_statement
  | enum_statement
  | fn_statement
  | mod_statement
  | use_statement
  | impl_statement
  | empty_statement
;

function_local_statement =
    let_statement
  | const_statement
  | loop_statement
  | empty_statement
  | expression
;

implementation_local_statement =
    const_statement
  | fn_statement
  | empty_statement
;

type_statement = 'type', identifier, '=', type ;
struct_statement = 'struct', '{', field_list, '}' ;
enum_statement = 'enum', '{', variant_list, '}' ;
fn_statement = 'fn', identifier, '(', field_list, ')', [ '->', type ], block_expression ;
mod_statement = 'mod', identifier ;
use_statement = 'use', path_expression ;
impl_statement = 'impl', identifier, '{', { implementation_local_statement }, '}' ;
const_statement = 'const', identifier, ':', type, '=', expression ;
let_statement = 'let', [ 'mut' ], identifier, [ ':', type ], '=', expression ;
loop_statement = 'for', identifier, 'in', expression, [ 'while', expression ], block_expression ;
empty_statement = ';' ;

(* Expressions *)
expression = operand_assignment, [ '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '<<=' | '>>=' | '|=' | '^=' | '&=', operand_assignment ] ;
operand_assignment = operand_range, [ '..' | '..=', operand_range ] ;
operand_range = operand_or, { '||', operand_or } ;
operand_or = operand_xor, { '^^', operand_xor } ;
operand_xor = operand_and, { '&&', operand_and } ;
operand_and = operand_comparison, [ '==' | '!=' | '>=' | '<=' | '>' | '<', operand_comparison ] ;
operand_comparison = operand_bitwise_or, { '|', operand_bitwise_or } ;
operand_bitwise_or = operand_bitwise_xor, { '^', operand_bitwise_xor } ;
operand_bitwise_xor = operand_bitwise_and, { '&', operand_bitwise_and } ;
operand_bitwise_and = operand_bitwise_shift, { '<<' | '>>', operand_bitwise_shift } ;
operand_bitwise_shift = operand_add_sub, { '+' | '-', operand_add_sub } ;
operand_add_sub = operand_mul_div_rem, { '*' | '/' | '%', operand_mul_div_rem } ;
operand_mul_div_rem = operand_as, { 'as', type } ;
operand_as = { '-' | '~' | '!' }, operand_access ;
operand_access = operand_path, {
    '[', expression, ']'
  | '.', integer | identifier
  | [ '!' ], '(', expression_list, ')'
} ;
operand_path = operand_terminal, { '::', operand_terminal } ;
operand_terminal =
    tuple_expression
  | block_expression
  | array_expression
  | conditional_expression
  | match_expression
  | struct_expression
  | literal
  | identifier
;

expression_list = [ expression, { ',', expression } ] ;

block_expression = '{', { function_local_statement }, [ expression ], '}' ;

conditional_expression = 'if', expression, block_expression, [ 'else', conditional_expression | block_expression ] ;

match_expression = 'match', expression, '{', { pattern_match, '=>', expression, ',' }, '}' ;

array_expression =
    '[', [ expression, { ',', expression } ] ']'
  | '[', expression, ';', integer, ']'
;

tuple_expression =
    '(', ')'
  | '(', expression, ')'
  | '(', expression, ',', [ expression, { ',', expression } ], ')'
;

struct_expression = identifier, '{', field_list, '}';

(* Parts *)
type =
    '(', ')'
  | 'bool'
  | 'u8' | 'u16' | 'u24' | 'u32' | 'u40' | 'u48' | 'u56' | 'u64'
  | 'u72' | 'u80' | 'u88' | 'u96' | 'u104' | 'u112' | 'u120' | 'u128'
  | 'u136' | 'u144' | 'u152' | 'u160' | 'u168' | 'u176' | 'u184' | 'u192'
  | 'u200' | 'u208' | 'u216' | 'u224' | 'u232' | 'u240' | 'u248' | 'field'
  | 'i8' | 'i16' | 'i24' | 'i32' | 'i40' | 'i48' | 'i56' | 'i64'
  | 'i72' | 'i80' | 'i88' | 'i96' | 'i104' | 'i112' | 'i120' | 'i128'
  | 'i136' | 'i144' | 'i152' | 'i160' | 'i168' | 'i176' | 'i184' | 'i192'
  | 'i200' | 'i208' | 'i216' | 'i224' | 'i232' | 'i240' | 'i248'
  | 'field'
  | '[', type, ';', expression, ']'
  | '(', type, { ',', type }, ')'
  | expression
;

pattern_match =
    boolean
  | integer
  | identifier
  | operand_path
  | '_'
;

field = identifier, ':', type ;
field_list = [ field, { ',', field } ] ;

variant = identifier, '=', integer ;
variant_list = [ variant, { ',', variant } ] ;

Keywords

Declarations

let
mut
const
type
struct
enum
fn
use
mod
impl
contract

Controls

for
in
while
if
else
match

Types

bool
u8 u16 ... u240 u248
i8 i16 ... i240 i248
field

Literals

true
false

Operators

as

Special

Self
self

Reserved

self
static
pub
ref
extern
return
loop
break
continue

Built-in functions

dbg

Prints its arguments to the terminal. Only for debugging purposes.

Arguments:

  • format string literal (str)
  • rest of the arguments to print

Return type: ()

Note: This function is special, as it accepts an arbitrary number of arguments of any type after the format string.

assert

Checks if the boolean expression is true. If it is not, the circuit fails with an error passed as the second argument.

Arguments:

  • boolean expression (bool)
  • error message string literal (str)

Return type: ()

Standard library

The standard library is unstable. Function signatures and behavior are going to be changed in future releases.

Most of the functions described here are special, as they accept arrays of arbitrary size. Since there are only fixed-size arrays in Zinc now, it would be challenging to create a function for arrays of every possible size. It is not possible to write such a function yourself using the language type system, but std makes an exception to simplify development for now.

Definitions

  • {scalar} - a scalar type, which can be bool, u{N}, i{N}, field
  • u{N} - an unsigned integer of bitlength N
  • i{N} - a signed integer of bitlength N
  • field - a field element of bitlength 254

std::crypto module

std::crypto::sha256

Computes the sha256 hash of a given bit array.

Will cause a compile-error if either:

  • preimage length is zero
  • preimage length is not multiple of 8

Arguments:

  • preimage bit array [bool; N]

Returns: 256-bit hash [bool; 256]

std::crypto::pedersen

Maps a bit array to a point on an elliptic curve.

Will cause a compile-error if either:

  • preimage length is zero
  • preimage length is greater than 512 bits

To understand what is under the hood, see this article.

Arguments:

  • preimage bit array [bool; N]

Returns: elliptic curve point coordinates (field, field)

std::crypto::ecc::Point

The elliptic curve point.


# #![allow(unused_variables)]
#fn main() {
struct Point {
    x: field,
    y: field,
}
#}

std::crypto::schnorr::Signature

The Schnorr EDDSA signature structure.


# #![allow(unused_variables)]
#fn main() {
struct Signature {
    r: std::crypto::ecc::Point,
    s: field,
    pk: std::crypto::ecc::Point,
}
#}

std::crypto::schnorr::Signature::verify

Verifies the EDDSA signature.

Will cause a compile-error if either:

  • message length is zero
  • message length is greater than 248 bits

Arguments:

  • the signature: std::crypto::schnorr::Signature
  • the message: [bool; N]

Returns: the boolean result

std::convert module

std::convert::to_bits

Converts a scalar value to a bit array of its bitlength.

Arguments:

  • scalar value: u{N}, or i{N}, or field

Returns: [bool; N]

std::convert::from_bits_unsigned

Converts a bit array to an unsigned integer of the array's bitlength.

Will cause a compile-error if either:

  • bit array size is zero
  • bit array size is greater than 248 bits
  • bit array size is not multiple of 8

Arguments:

  • bit array: [bool; N]

Returns: u{N}

std::convert::from_bits_signed

Converts a bit array to a signed integer of the array's bitlength.

Will cause a compile-error if either:

  • bit array size is zero
  • bit array size is greater than 248 bits
  • bit array size is not multiple of 8

Arguments:

  • bit array: [bool; N]

Returns: i{N}

std::convert::from_bits_unsigned

Converts a bit array to a field element.

Arguments:

  • bit array: [bool; 254]

Returns: field

std::array module

std::array::reverse

Reverses a given array.

Arguments:

  • array: [{scalar}; N]

Returns: [{scalar}; N]

std::array::truncate

Truncates an array of size N to an array of size new_length.

Will cause a compile-error if either:

  • array size is lesser than new length
  • new length is not a constant expression

Arguments:

  • array: [{scalar}; N]
  • new_length: u{N} or field

Returns: [{scalar}; new_length]

std::array::pad

Pads a given array with the given values.

Will cause a compile-error if either:

  • array size is greater than new length
  • new length is not a constant expression

Arguments:

  • array: [{scalar}; N]
  • new_length: u{N} or field
  • fill_value: {scalar}

Returns: [{scalar}; new_length]

std::ff module

std::ff::invert

Inverts a finite field.

Arguments:

  • value: field

Returns: field