noname 3.0: Native Hints, Standard Library, Compiler Visualizer, And More!
on

noname-v3

We’ve been working on noname for a while now, a zk programming language inspired by Rust and Golang, with the goal to provide a better experience than Circom for developers. We’re excited to announce that noname should now achieve full feature parity with Circom.

We introduce noname 3.0, the most important update to noname which includes native hints, a standard library (stdlib), more debugging features, and many more quality-of-life improvements for developers. In this post, we’ll discuss these updates in more detail and outline our priorities for the next phase of noname’s development.

Hint Function

Due to various limitations of arithmetic backends, many calculations have to be done outside of the circuit. Take the division operation, for example:

res = dividend / divisor

In the R1CS backend, each constraint is expressed in the quadratic form a * b = c. To make division work in such a constraint system, we must transform the original equation into res * divisor = dividend. Given the variables dividend and divisor, we need to calculate the value for res and then make it part of the constraint to evaluate. This calculation is called hint calculation.

Out-of-circuit calculation is another name for hint calculation. The reason it’s considered “out of circuit” is that while the hint value becomes part of the circuit, the hint calculation process itself is not part of the circuit. In other words, the steps in the hint calculation are not constrained.

Therefore, there is a trade-off when using hint functionality in a ZK compiler. On the one hand, it fosters innovation, as people can do whatever they want with hints in a circuit. On the other hand, it can introduce serious security issues if hints are not properly constrained. The trade off is to introduce an unsafe keyword similar to the one in Rust to emphasize the potential risks when using hints. Here is a simple example:

hint fn div(dividend: Field; divisor: Field) -> Field {
	return dividend / divisor;
}

fn constrained_div(xx: Field, yy: Field) -> Field {
    // will throw error if `unsafe` is not present
	let res = unsafe div(xx, yy);

	// this relation proves res is the division result
	assert_eq(res * yy, xx);

	return res;
}

The requirement for the keyword unsafe when calling a hint function is meant to have users acknowledge that the relationship between the variable res and the div function is not constrained. This means any value can be assigned to the variable res.

The necessity of the unsafe keyword helps raise awareness of the fact that it is the user’s responsibility to ensure that the necessary constraints are in place to achieve the intention of the hint calculation. For example, the assert_eq(res * yy, xx) is a manual constraint to ensure that the relationship between the inputs and output holds for the hint function div.

Initial Standard Library

This is the first update for noname’s standard library, which is crucial for practical projects, as writing circuits often requires handling low-level programming details. Creating circuits such as for bit decomposition and comparator can be daunting for beginners, as they require a deep understanding of the arithmetic backends and the security implications.

The initial standard library has the most basic modules: std::bits, std::comparator, std::mimc, std::multiplexer and std::ints.

Below provide examples for each of them.

Bit Decomposition

This is the most fundamental module in the stdlib. Many circuits will depend on it, particularly for range checking.

signatures

fn to_bits(const LEN: Field, value: Field) -> [Bool; LEN]
fn from_bits(bits: [Bool; LEN]) -> Field

example

use std::bits; // import the `bits` module

fn main(pub xx: Field) {
	// decompose the value of xx variable as 3 bits
	let bits = bits::to_bits(3, xx);

	// assume xx = 2, the bits will be [0, 1, 0]
	assert(!bits[0]);
	assert(bits[1]);
	assert(!bits[2]);

	// convert bits back to a field value
	let val = bits::from_bits(bits);
	assert_eq(val, xx);
}

Comparator

Another basic module is the comparator, which depends on the bits module.

signature:

fn less_than(const LEN: Field, lhs: Field, rhs: Field) -> Bool

example:

use std::comparator; // import comparator module

fn main(pub lhs: Field, rhs: Field) -> Bool {
	// should return true when lhs < rhs, otherwise false
	let res = comparator::less_than(3, lhs, rhs);

	return res;
}

Unsigned Integers

Without encapsulation, it is easy to make mistakes when using low-level APIs like the comparator, which require certain assumptions about function parameters. If the provided value for the argument doesn’t match the assumption, it could lead to soundness issues.

To mitigate issues stemming from misunderstandings of low-level APIs, UInts are represented by structs in noname. The struct methods encapsulate the functions over the struct’s inner value. For example, the Uint8.new function ensures that the value representing a Uint8 has been range-checked.

struct Uint8 {
	inner: Field,
}

fn Uint8.new(val: Field) -> Uint8 {
	let bit_len = 8;
	bits::check_field_size(bit_len);

	// range check
	let ignore_ = bits::to_bits(bit_len, val);

	return Uint8 {
		inner: val
	};
}

With this base struct in place, it can add methods to the struct.

Take the less_than method for example:

fn Uint8.less_than(self, rhs: Uint8) -> Bool {
	return comparator::less_than(8, self.inner, rhs.inner);
}

The struct helps encapsulate low-level APIs like comparator::less_than. This way, users don’t need to deal with low-level parameters, helping to avoid security mistakes. This approach offers flexibility in managing both low- and high-level APIs.

In addition, UInts currently support basic operations such as addition, subtraction, multiplication, division, modulo, etc.

MiMC

MiMC is a simple hash algorithm. In noname, the main function is mimc7_hash, which uses a low-level API mimc7_cipher for its core function. We also hardcoded the number of rounds to avoid potential misuse.

signature:

fn mimc7_hash(values: [Field; LEN], key: Field) -> Field

example:

let key = 321;
let val = [1, 2, 3]; // an array of values to hash
let res = mimc::mimc7_hash(val, xx); // return the hash

Multiplexer

In a circuit, accessing array elements isn’t as straightforward as it is in conventional programming languages. At low level, it requires certain arrangements of the inputs and outputs to fulfill necessary constraints. The multiplexer module is a high-level API that makes accessing array elements easier.

signature:

// Selects an element from a 2D array based on a `target_idx` and returns a vector of length `WIDLEN`.
fn select_element(arr: [[Field; WIDLEN]; ARRLEN], target_idx: Field) -> [Field; WIDLEN]

example:

let xx = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
let idx = 1
// should return [4, 5, 6]
let chosen_elements = multiplexer::select_element(xx, idx);

Iterator

A newly added language feature is the iterator, which works well with generic arrays. The generic feature enables working with arrays of varying lengths, while the iterator feature simplifies looping through these generic arrays.

example:

fn main(pub arr: [Field; LEN]) {
	let mut sum = 0;

	// without iterator
	for idx in 0..LEN {
		sum = sum + arr[idx];
	}

	// using iterator
	for elm in arr {
		sum = sum + elm;
	}
}

Logging

It is nearly impossible to build something interesting without a proper logging facility. A convenient logging function can save developers a huge amount of time. Noname recently added its own logging function to facilitate this need.

examples:

fn main(pub public_input: Field, private_input: Field) {
	let xx = private_input + public_input;
	let yy = private_input * public_input;
	log(xx); // will print out something like "span:147, val: 4"
	log(yy); // "span:148, val: 4"
	assert_eq(xx, yy);
}
fn main(pub xx: Field) {
	let mut thing = Thing { xx: xx, };
	log(thing.xx); // print out a field of a struct
	thing.xx = thing.xx + 1;
	log(thing.xx);
}

Currently, it only supports printing out a field. There is significant room to improve this feature. One planned improvement is to allow customized message per logging (also listed at the end of this post).

Compiler Pipeline Visualizer

Noname’s compiler has a series of pipeline steps that compile code into a circuit. Investigating how code is compiled requires a deep understanding of how the compiler works, as well as manual logging in the noname codebase.

We built a compiler visualizer to help investigate the compiling process. This tool can also serve as a learning tool for understanding how a compiler works. For example, you can write your own noname code and use this tool to visualize how your code is compiled step by step.

To use it, run the following command from your noname package’s root folder:

noname build --server-mode

In the screenshot below, two panels are displayed side by side, showing the state of the compiler at different point in time.

visualizer

These panels are used for comparing the differences between two different artifacts in the compiling process.

For example, each of the rows in the gray areas of the panels is clickable, showing their corresponding details. To investigate how the lexer of the bits module is compiled into the AST, you can click the first element in panel 1 and the second element in panel 2. This allows you to check the lexer tokens on the left-hand side and the resulting AST on the right-hand side.

What Is Next?

With the debuggers, hint functions and initial stdlib in place, noname is now much closer to a position for practical ZK projects to write their circuits. To take it further, below are the prioritized tasks we will work on.

We will add more stdlib to cover more use cases. These include the boolean type, sparse merkle tree and bigint etc.

Also there will be improvements on the language features: Allow expressions in ITE branches: The current if else branches only allow a single variable. Allowing expressions in the conditional branches will enable writing complex logic when necessary.

Better logger: Support customized message for each logging. This should greatly improve the dev experiences.

Allow complex package structure: This will allow building complex noname 3rd party packages and help manage the code structure.

Dynamic module loading: Instead of loading all builtins and all stdlib, this will improve the efficiency of the compiling process as the dynamic module loading will remove the unnecessary prologue.

Accessibility control over struct fields: This basically provide a “whitelist” for struct fields, to avoid struct API misuse.

Generic struct: The generic function feature allows creating function templates for different cases based on their constant values. It greatly improve the code reusability while simplifies the codebase. Generic struct feature is for the exact same purpose, but for the structs.

In addition, here are some easy but impactful tasks:

We appreciate contributions. If you want to work on any of these features or have any suggestions, please feel free to reach out.