ZK/SEC Research notes from zkSecurity
All posts
noname · Part 4 of 4

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

noname 3.0

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:

  • array utils: https://github.com/zksecurity/noname/issues/156
  • snapshot tests: https://github.com/zksecurity/noname/issues/200
  • boolean constructor: https://github.com/zksecurity/noname/issues/213

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

Keep reading
Recommended

noname 2.0: Unlocking Numeric Generics, Folding Schemes, and a Playground

We're excited to introduce the preview of noname 2.0, packed with features that make developing advanced ZK circuits easier than ever. This update includes flexible generic-sized arrays, seamless integration with folding schemes for IVC, and an interactive online playground to test and share code. We've also optimized R1CS constraint generation to boost performance. Plus, there are numerous community-driven enhancements and bug fixes that make the language more robust and user-friendly. Dive in to explore the specifics of our journey, learn from the contributions of our vibrant open-source community, and see how noname is evolving into a more versatile tool for developers.

ZK/SEC · August 08, 2024

noname: ZK app developers should be able to see down to the constraints

Zero-knowledge apps are evolving, and we've been diving into their two main forms: VM instructions and arithmetic circuits. Understanding the "assembly" layer is crucial for developers, especially when optimizing and ensuring security. We’ve played around with a new toy language called **noname**, blending Golang and Rust vibes to make zkApps more understandable. With **noname**, you get detailed insights about how your code translates into gates, offering a clearer picture of the underlying "assembly" and helping pinpoint compiler bugs. If you're curious about enhancing your low-level programming skills or peeking into circuit construction, check out our experiments and see if this inspires you to create better debugging tools!

David Wong · June 03, 2023

The zero-knowledge attack of the year might just have happened, or how Nova got broken

Last week, a surprising paper revealed a major vulnerability in Microsoft's Nova, a leading zero-knowledge proof (ZKP) system, that showcased a false computation with a valid proof. This discovery is especially intriguing given the complexity and robustness typically associated with ZKP systems. We had just launched our company to tackle bugs in ZKP frameworks, and while we anticipated issues, complete breakdowns of systems themselves are rare. Nova's issue revolved around cycles of elliptic curves, showing how small implementation oversights in complex cryptographic systems can lead to significant errors. This post dives into the intricate mechanics of Nova and the discovery process, offering a fascinating glimpse into why rigorous specifications are crucial in cryptography.

David Wong · July 02, 2023
More to explore

Introducing bugs.zksecurity.xyz a knowledge base for ZK bugs

We're thrilled to introduce our new site, [bugs.zksecurity.xyz](https://bugs.zksecurity.xyz/), a hub for exploring past vulnerabilities in ZK circuits. Dive into our growing catalog of documented bugs and learn how we've reproduced some with comprehensive scripts. Discover evaluations of prominent security tools like Circomspect and Picus, and see where they shine or stumble. We're calling on the community to join us in expanding this invaluable resource, whether by adding bugs, reproducing them, or improving our platform. Let's collaborate to elevate ZK security together!

Stefanos Chaliasos · February 17, 2025

Archetype x zkSecurity - Proof is in the Pudding: ZK on Bitcoin

In Session 09 of "Proof is in the Pudding," we explore the intersection of zero-knowledge proofs and Bitcoin. We break down Bitcoin's UTXO model and Script limitations, then dive deep into approaches for verifying ZK proofs on Bitcoin, from MPC-based techniques to BitVM's optimistic verification with fraud proofs. We cover timelocks, the statelessness problem and Lamport signatures for state, Taproot, simulated covenants, BitVM 3 with hashlocks and garbled circuits, cut-and-choose security, and witness encryption (BABE).

ZK/SEC · April 02, 2026

Why does FRI work?

This blog post explains the security intuition behind the FRI protocol, which proves that a function is close to a valid Reed-Solomon codeword. It introduces the "prover message graph," a layered structure that visualizes how correct and incorrect folds affect verification. We conclude that if too many folds are inconsistent, the verifier will likely reject, but if most are correct, the initial function must be close to a proper codeword.

Nicolas Mohnblatt · October 30, 2025