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

landing

We are thrilled to unveil the upcoming release of noname 2.0 preview; a major leap forward in the noname language! This release will introduce powerful enhancements that enable developers to write more sophisticated ZK circuits. The notable enhancements include:

  • Generic-Sized Arrays: Unlock greater flexibility and effectiveness in your circuit code by enabling functions to handle arrays of varying sizes.
  • Folding Schemes via Sonobe Integration: Seamlessly create your circuits for IVC (incremental verifiable computation). Particularly useful for applications like: Bitcoin light clients on EVM, rollups, and zkVMs.
  • Interactive Online Playground: Experiment, learn, and share your code effortlessly with a new web-based platform.
  • R1CS Optimization: Improve performance by reducing the number of constraints generated from R1CS backend.
  • Bugfixes and Life improvements: A number of community contributions have significantly improved the language’s usability and stability.

These enhancements, coupled with the invaluable contributions from our vibrant open-source community, have transformed noname into an even more versatile and user-friendly language. In this post, we will discuss the specifics of these updates, outline our journey in bringing them to life, and acknowledge the significant contributions from our community that have shaped this progress.

Generic-Sized Arrays

One of our ongoing efforts is enhancing the standard library, making it easier for developers to get started with noname. A key challenge we faced was enabling the language to handle functions that accept and return arrays of varying sizes.

Initially, we considered managing without explicit generic features, inspired by GoLang’s long history without them. However, given that noname is a typed DSL with a strong emphasis on type checking, we realized the necessity of supporting generic types.

To address this, we introduced generic syntax into the language. Initially, we used the conventional syntax <P> next to the function name to denote generic parameters. For example:

fn to_bits<LEN>(val: Field) -> [Field; LEN]
fn from_bits<LEN>(bits: [Field; LEN]) -> Field

Our primary goal was to minimize codebase changes while implementing the generic-sized array feature.

We initially planed to relax certain type checks for generic types, deferring them to the circuit synthesizer phase. This approach is similar to the concept dynamic dispatch, retaining and managing the underlying structures behind a pointer during the synthesizer phase to propagate constant values and resolve generic types.

However, this approach doesn’t resolve the generic types before proceeding to the synthesizer phase after the compilation pipeline. This required significant changes to the synthesizer’s assumptions, which are heavily based on the concrete type.

Eventually, we adopted another approach called monomorphization. Instead of heavily refactor the cicuit synthesizer, the generic types are processed and resolved into concrete types by the monomorphization phase in the pipeline before being passed to the circuit synthesizer phase. The following diagram depicts the pipeline.

pipeline

With monomorphization, the synthesizer remains unchanged and doesn’t need to accommodate the new generic syntax. This approach significantly reduces complexity and potential risks compared to type erasure.

Let’s take a look at how the monomorphization works in the code level:

fn main() {
	// As to_bits / from_bits are generic functions,
	// this will create a new function AST instance for bit length of 3
	let bits = to_bits::<3>(2);
	let val = from_bits::<3>(bits);

	assert_eq(val, 2);
}

After the pass of monomorphization in the pipeline, it instantiates generic functions based on different constant values for the generic parameters. These two function calls will point to the new function instances instead of the original generic function AST.

Function instantiation process will convert the generic types to concrete types, such that:

to_bits::<3>(2);
    
// the function call above will instantiate the to_bits function as following 
fn to_bits(val: Field) -> [Field; 3]

So the generic type for the return [Field; LEN] became concrete type [Field; 3], after monomorphization.

We also realized that the turbofish syntax was unnecessarily verbose. The detailed reasons are explained in the RFC.

It ends up having the following syntax to express generic function, which is less verbose and more natural to use:

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

This allows the function calls to be simplified as:

fn main() {
	let bits = to_bits(3, 2);
	let val = from_bits(bits);

	assert_eq(val, 2);
}

For more details on the monomorphization design process, please refer to our first RFC. We are still looking for feedback on the design and implementation, so feel free to share your thoughts and suggestions.

Folding via Sonobe

Sonobe is an IVC (Incremental Verifiable Computation) framework that simplifies the development of circuits for folding schemes. It effectively abstracts the complexities of folding schemes, allowing developers to primarily focus on implementing the core IVC logic.

folding

Sonobe facilitates the integration of IVC circuits by handling the surrounding infrastructure. Developers can concentrate on defining the circuit for the function 𝐹, as illustrated in the diagram. It then manages the sequencing of computations and generates a proof to verify the correctness of the IVC process.

The potential application involves computing a proof for a series of Bitcoin blocks’ transitions off-chain and then verifying it on the Ethereum Virtual Machine (EVM). This means that it’s possible to implement a Bitcoin light client on the EVM. Another interesting application could be proving computations for a virtual machine (VM). Since the computation logic for opcodes in the VM is fixed, feeding opcodes along with outputs from an initial state consistently leads to a deterministic final state.

To enable noname to support folding schemes via Sonobe, we identified two straightforward requirements:

  • Compatibility with the arkwork R1CS format.
  • Integration of noname as a new frontend within Sonobe.

While noname already supports generating R1CS circuits compatible with the SnarkJs toolkit, it currently relies on the Circom Rust library rather than the arkwork R1CS library. Therefore, it was necessary to create a port for the latter.

Pierre, a core contributor of Sonobe, initiated researching for noname integration. He suggested noname to support array public output, an important feature. Remarkably, erhant from the community quickly implemented the suggestion, and finiteprods extended the feature to support all types as public outputs.

A special thanks to Pierre and all the contributors who played a crucial role in the Sonobe integration development. Now, you can write circuits in noname that incorporate folding schemes seamlessly.

Online Playground

The online playground is an invaluable tool for quickly sharing and testing code snippets. Logan identified this need and created a suggestion to propose a development, subsequently taking the initiative to implement the online playground.

playground1

As shown in the screenshot, the web UI offers a much more intuitive experience for navigating features compared to the terminal interface. It supports compiling and running noname code with user inputs. Additionally, it provides options to select the backend for displaying the assembly output, which is useful for checking and debugging constraints.

playground2

Thanks to Logan’s efforts, the online playground represents a significant advancement in enriching the development experience, particularly in prototyping, testing, and debugging noname code. Even in the absence of a language server for linting and syntax highlighting in editors, this playground addresses existing gaps by providing an additional debugging tool with user-friendly UI. It potentially lays the groundwork for future enhancements that will continue to improve the developer experience and streamline the development process.

R1CS Optimization

After the refactor to support multiple backends, we have added an R1CS (Rank-1 Constraint System) backend to noname as mentioned in our previous blog post. Initially, each operation generated a constraint in R1CS, which was a straightforward but naive approach. The performance of the proving system is significantly impacted by the size and number of constraints, making optimization crucial.

To enhance performance, we optimized R1CS constraint generation by accumulating linear combinations and only adding constraints when necessary. Specifically, constraints are required in cases of comparing equality or arithmetic multiplication operations.

This optimization has led to a significant reduction in the number of constraints generated. For example, consider the constraints generated for the iterate.no example:

Before optimization:

v_3 == (v_1 + -1) * (1)
v_4 == (v_3 + 3) * (1)
v_4 == (v_2) * (1)

After optimization:

v_1 + 2 == (v_2) * (1)

The first two constraints are generated for the addition and subtraction operations, which can be accumulated as an intermediate linear combination. Then, when asserting equality, the accumulated linear combination is used to generate the necessary constraint.

While this optimization significantly improves performance, it presents a challenge for debugging since constraints no longer map one-to-one with the code. To address this, we are planning to retain all intermediate linear combinations so that corresponding operations can be traced back to the code. This debug information could be exported as a JSON file and displayed in the playground, aiding developers in debugging and ensuring the accuracy of constraints.

Other Contributions from the Community

In addition to these major updates, numerous community contributions have significantly enhanced noname. We would like to pay tribute to the following invaluable efforts by the community members. We are immensely grateful for their support.

Enhancements

Fixes

Documentation

Conclusion

We hope these organic enhancements provide developers with greater flexibility, efficiency, and a more intuitive development experience. Again, we extend our heartfelt gratitude to all contributors who have been a part of this journey.

In case you are interested in knowing more about how the noname internal works, you can check out our more recent code walkthrough video.

More resources: