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

noname meets Ethereum: Integration with SnarkJS

We are thrilled to announce that noname, zkSecurity's programming language to write ZK circuits, now supports R1CS! This update allows developers to write ZK circuits in a rust-and-golang-inspired language and deploy them to Ethereum using SnarkJS. This offers an alternative to the widely-used Circom language for zk-SNARK proofs on Ethereum. In this post, you'll see the first noname circuit deployed on Ethereum, and we'll show you how to deploy one yourself.

noname & snarkjs

The zero-knowledge field is evolving rapidly, with new proof systems and frameworks frequently emerging. Developers face a major challenge: fragmentation. Each proof system has its own language and quirks, often targeting specific projects and unavailable on other platforms. For example, Noir is for Aztec Network, Leo is for Aleo, Cairo is for Starknet, and O1js is for Mina.

What if there was a way to bridge this gap? A language that could unify the zk ecosystem, allowing developers to write circuits that work across different backends.

This is what we're doing! while noname was first created to compile programs to work with Mina's kimchi proof system, we are now updating it to work with Ethereum's SnarkJS.

The integration of noname with R1CS and SnarkJS is a pivotal step in the language's journey, and it's been made possible thanks to a grant from the Ethereum Foundation's Privacy & Scaling Explorations team.

A Sudoku as a noname program

Using Sudoku as an example demonstrates two benefits of zero-knowledge proofs:

  1. Proving correctness of a Sudoku solution without revealing the solution itself.

  2. Reducing on-chain computation costs. The verification cost on-chain is constant, regardless of the complexity of the off-chain computation.

The full code for the sudoku circuit is available here. Let's look at our main function:

fn main(pub grid: Sudoku, solution: Sudoku) {
    solution.matches(grid);
    solution.verify();
}

It receives two arguments: the Sudoku problem as a public input, and the solution as a secret.

At a high level, this sudoku circuit checks:

  1. the solution grid matches with the sudoku problem (encoded in the public inputs)

  2. verify the solution indeed follows the sudoku rules

In Noname, grid: Sudoku automatically interprets inputs as the Sudoku type:

struct Sudoku {
    inner: [Field; 81],
}

Here is how it checks if the solution matches with the grid of a solution problem.

// return the value in a given cell
fn Sudoku.cell(self, const row: Field, const col: Field) -> Field {
    return self.inner[(row * 9) + col];
}

// verifies that self matches the grid in places where the grid has numbers
fn Sudoku.matches(self, grid: Sudoku) {
    // for each cell
    for row in 0..9 {
        for col in 0..9 {
            // either the solution matches the grid
            // or the grid is zero
            let matches = self.cell(row, col) == grid.cell(row, col);
            let is_empty = grid.cell(row, col) == empty;
            assert(matches || is_empty);
        }
    }
}

Next, the program verifies the solution according to sudoku roles:

fn Sudoku.verify(self) {
    self.verify_rows();
    self.verify_cols();
    self.verify_diagonals();
}

fn Sudoku.verify_rows(self) {
    for row in 0..9 {
        for num in 1..10 {
            let mut found = false;
            for col in 0..9 {
                let found_one = self.cell(row, col) == num;
                found = found || found_one;
            }
            assert(found);
        }
    }
}

fn Sudoku.verify_cols(self) {
    for col in 0..9 {
        for num in 1..10 {
            let mut found = false;
            for row in 0..9 {
                let found_one = self.cell(row, col) == num;
                found = found || found_one;
            }
            assert(found);
        }
    }
}

fn Sudoku.verify_diagonals(self) {
    for num in 1..10 {

        // first diagonal
        let mut found1 = false;
        for row1 in 0..9 {
            let temp1 = self.cell(row1, row1) == num;
            found1 = found1 || temp1;
        }
        assert(found1);

        // second diagonal
        let mut found2 = false;
        for row2 in 0..9 {
            let temp2 = self.cell(8 - row2, row2) == num;
            found2 = found2 || temp2;
        }
        assert(found2);
    }
}

As it is shown above, the typing support behind the language makes circuit code straightforward, akin to writing in conventional languages.

Plugging into Snarkjs

Now that we have the sudoku code, we can use the noname CLI to generate and run circuits that are compatible with Snarkjs.

  1. Install noname

    cargo install --git https://www.github.com/zksecurity/noname
    

  2. The command guideline

noname-help

  1. Generate r1cs circuit and witness files compatible with snarkjs
noname run --backend r1cs-bn254 --path ../test/noname-sudoku \
--private-inputs '{"solution": { "inner": ["9", "5", "3", "6", "2", "1", "7", "8", "4", "1", "4", "8", "7", "5", "9", "2", "6", "3", "2", "7", "6", "8", "3", "4", "9", "5", "1", "3", "6", "9", "2", "7", "5", "4", "1", "8", "4", "8", "5", "9", "1", "6", "3", "7", "2", "7", "1", "2", "3", "4", "8", "6", "9", "5", "6", "3", "7", "1", "8", "2", "5", "4", "9", "5", "2", "1", "4", "9", "7", "8", "3", "6", "8", "9", "4", "5", "6", "3", "1", "2", "7"] }}' \
--public-inputs '{"grid": { "inner": ["0", "5", "3", "6", "2", "1", "7", "8", "4", "0", "4", "8", "7", "5", "9", "2", "6", "3", "2", "7", "6", "8", "3", "4", "9", "5", "1", "3", "6", "9", "2", "7", "0", "4", "1", "8", "4", "8", "5", "9", "1", "6", "3", "7", "2", "0", "1", "2", "3", "4", "8", "6", "9", "5", "6", "3", "0", "1", "8", "2", "5", "4", "9", "5", "2", "1", "4", "9", "0", "8", "3", "6", "8", "9", "4", "5", "6", "3", "1", "2", "7"] }}'

Then it should generate the .r1cs and .wtns files and tell you where the files are located.

R1CS file generated at: ../test/noname-sudoku/output.r1cs
Witness file generated at: ../test/noname-sudoku/output.wtns
  1. Test the generated r1cs circuit and witness files

There is a script to quickly test the compatibility of these outputs with the SnarkJS. The script automatically generates the proof and verifies it off-chain via SnarkJS.

Verify the proof on Ethereum

The snarkjs provides a command to export a solidity verifier. We have a deployed instance on Ethereum at 0x7c686a33ac3f2c911b03b9aa80e5175d3ab4152a.

To test a solution on-chain, you can use this SnarkJS command to generate the calldata.

Intuitively, the calldata contains the contract call arguments to represent a sudoku problem (the public inputs in the circuit) and a proof for the corresponding solution (without revealing it).

Screenshot 2024-05-30 at 16.23.34

As the screenshot shown (from remix), if the proof(_pA, _pB, _pC) and the sudoku problem matches (_pubSignals), then the contract call to verifyProof should return true.

What's next?

noname is a work-in-progress language, and many features are missing! We're looking for help and contributions. If you're interested, check the issues.

To learn more about Noname, please check out the repo, or the following blog posts and videos:

Keep reading
Recommended

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

We're super excited to introduce noname 3.0, our zk programming language inspired by Rust and Golang, now achieving full feature parity with Circom. This update brings native hints, a standard library, debugging features, and a lot more to enhance developer experience. Dive into how hint functions work with an 'unsafe' keyword to balance innovation and security, explore our new stdlib modules, and see how the compiler pipeline visualizer can help you understand the compiling process. Plus, check out our next steps and how you can contribute to shaping noname's future.

ZK/SEC · November 13, 2024

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
More to explore

Uncovering and Fixing an Inflation Bug in Aleo

In November 2024, we found a significant inflation bug in the Aleo mainnet that could have allowed token minting without proper checks. We immediately informed the Aleo team, who swiftly addressed the issue with no detected exploitation. This post dives into the inner workings of Aleo and explains how transitions and records operate, providing insight into how the vulnerability was discovered and resolved. It's an intriguing look at blockchain security, zero-knowledge proofs, and the importance of thorough type checks to ensure robust protocol integrity.

Suneal Gong · February 19, 2025

Public report of auditing Penumbra's circuits

We conducted an audit of Penumbra's main circuits and found eight issues, including the critical "double spend" and "double vote" bugs, which the Penumbra team promptly fixed. Our findings highlight Penumbra's robust documentation and code testing. Readers will get insights into how Penumbra uses zero-knowledge proofs for privacy, its decentralized exchange features, and its governance model. The post also provides detailed pseudocode for various cryptographic protocols, emphasizing how Penumbra ensures secure and private transactions. It's a deep dive into the technical details for those intrigued by privacy-focused blockchain technologies.

ZK/SEC · August 24, 2023

The State of Security Tools for ZKPs

Zero-knowledge proofs (ZKPs) have come a long way from theory to real-world applications like blockchains and private transactions. We’ve been busy auditing various ZKP implementations and developing tools to improve circuit safety and security. In this blog post, we’ll explore how vulnerabilities can crop up in SNARK systems and the current state of tools designed to spot these issues. From circuit bugs to the often-overlooked frontend and backend layers, we cover how various analysis techniques and formal verification approaches are evolving to ensure robust ZKP systems. Dive in to discover the potential and current challenges in ZKP security!

ZK/SEC · June 02, 2024