Module 4: Introduction to Noir Programming Language
Learning Objectives
By the end of this lesson, you will be able to:
- Understand the purpose and design philosophy of Noir
- Set up the Noir development environment
- Identify the key syntactic elements of Noir
- Write and compile basic Noir programs
- Distinguish between different types of Noir functions
- Explain how Noir compiles to ZKP circuits
- Analyze Noir's type system and its relationship to ZKP mathematics
- Implement complex circuits using Noir's advanced features
- Debug and optimize Noir circuits for performance
Table of Contents
- Introduction
- The Philosophy Behind Noir
- Setting Up Your Noir Development Environment
- Basic Noir Syntax
- Core Noir Concepts
- Noir's Compilation Process
- Noir's Type System
- Noir Standard Library
- Proving and Verification with Nargo
- Advanced Noir Features
- Circuit Optimization in Noir
- Debugging and Troubleshooting
- Real-World Noir Applications
- Visual Representations and Diagrams
- Knowledge Check
- Summary
- Enhanced Exercises with Solutions
Introduction
Welcome to Noir, the domain-specific language (DSL) designed specifically for writing zero-knowledge proof circuits! Created by the Aztec Network team, Noir aims to make ZKP development as accessible as traditional smart contract development. In this module, we'll explore Noir's syntax, core concepts, and how it abstracts away the complexity of underlying ZKP mathematics.
The need for ZKP-specific languages arises because writing ZKP circuits directly with the underlying mathematics would be extremely complex and error-prone. Just as Solidity abstracts away Ethereum's execution model, Noir abstracts away the complex mathematics of zero-knowledge proofs. This allows developers to focus on expressing their computation logic rather than worrying about the underlying cryptographic implementation.
Analogy: Think of Noir as a high-level programming language for ZKPs, similar to how Python abstracts away the complexity of machine code. Just as you don't need to write assembly language to develop web applications, you don't need to do complex mathematical conversions to write ZKP circuits in Noir.
The Philosophy Behind Noir
Why a New Language?
Noir was created with these core principles in mind:
- Accessibility: Make ZKP development approachable for mainstream developers who may not have deep mathematical backgrounds
- Safety: Provide compile-time checks, type safety, and circuit validation to prevent common errors
- Expressiveness: Allow developers to express complex constraints naturally using familiar programming paradigms
- Abstraction: Hide the complexity of underlying ZKP mathematics while providing escape hatches when needed
- Performance: Automatically optimize circuits during compilation for efficient proof generation and verification
- Domain-Specific: Tailored specifically for the unique requirements of ZKP development
Noir vs. Traditional Circuit Programming
Traditional Approach (e.g., directly with mathematical concepts):
- Requires deep understanding of finite fields, polynomial arithmetic, and elliptic curves
- Manual conversion of logical operations to arithmetic constraints
- Error-prone due to complex mathematical manipulations
- Time-consuming to implement even simple circuits
Noir Approach:
- Familiar syntax similar to Rust
- Automatic constraint generation from high-level code
- Built-in safety features and type checking
- Focus on logic rather than mathematics
Noir's Design Goals
- Familiar Syntax: Borrow concepts from Rust and modern programming languages, making it approachable for existing developers
- Constraint-Oriented: Express mathematical constraints naturally through familiar control structures
- Domain-Specific: Tailored specifically for ZKP development with built-in cryptographic operations
- Compiler Optimization: Automatically optimize circuits for constraint count, proof size, and generation time
- Backend Agnostic: Support for multiple ZKP backends without changing source code
Setting Up Your Noir Development Environment
Prerequisites
Before installing Noir, ensure you have:
- A Unix-like environment (Linux, macOS, or Windows with WSL)
- Git installed
- Basic command-line knowledge
- Rust toolchain (optional but recommended for advanced usage)
Installing Nargo
Nargo is Noir's package manager and build system (similar to Cargo for Rust). It handles compilation, proof generation, and verification.
Installation Method 1: Using noirup (Recommended)
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install_noirup.sh | bash
source ~/.bashrc
noirup -v latest
Installation Method 2: Using Package Managers
# On macOS with Homebrew
brew install noir-lang
# On Linux (Ubuntu/Debian)
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install_noirup.sh | bash
source ~/.bashrc
noirup -v latest
Verification of Installation
nargo --version
Creating Your First Noir Project
# Create a new Noir project
nargo new my_zkp_project
cd my_zkp_project
# Explore the project structure
ls -la
# You should see:
# ├── src/
# │ └── main.nr # Your Noir code goes here
# ├── Nargo.toml # Project configuration
# └── Prover.toml # Prover inputs for testing (optional)
Project Structure Explained
my_zkp_project/
├── src/
│ └── main.nr # Main Noir source file
├── Nargo.toml # Project configuration (name, version, dependencies)
├── Prover.toml # Prover input values for testing
├── Verifier.toml # Verifier inputs (if needed)
└── target/ # Compiled artifacts (auto-generated)
Essential Nargo Commands
# Check syntax and compile without generating proof
nargo check
# Generate a proof with default or configured inputs
nargo prove
# Verify an existing proof
nargo verify
# Build the project (compilation only)
nargo build
# Run tests
nargo test
# Update Noir to the latest version
noirup -v latest
Basic Noir Syntax
Hello World Circuit
// src/main.nr
fn main(x: Field, y: pub Field) {
assert(x != 0);
assert(y == x * x);
}
This simple circuit proves that y is the square of x, without revealing x itself. The prover knows x (private input), and the verifier sees that y (public input) is indeed the square of some value.
Key Syntax Elements
Data Types in Noir
1. Field Type
The Field type represents elements in a prime field (typically a large prime field like the BN254 scalar field). It's the primary data type for computations in ZKP circuits.
// Basic Field operations
fn basic_operations(a: Field, b: Field, result: pub Field) {
let sum = a + b;
let diff = a - b;
let product = a * b;
let division = a / b; // Only valid if b != 0
assert(result == sum + diff + product + division);
}
2. Boolean Type
The bool type represents Boolean values (true/false).
fn boolean_example(condition: bool, value: Field, result: pub Field) {
if condition {
assert(result == value * 2);
} else {
assert(result == value * 3);
}
}
3. Unsigned Integer Types
Noir supports various unsigned integer types: u8, u16, u32, u64, which are useful for specific operations and range checks.
fn integer_operations(a: u32, b: u32, result: pub u32) {
let sum: u32 = a + b;
let product: u32 = a * b;
// Operations are performed in the field, but with bit-width constraints
assert(result == sum + product);
}
4. Arrays Fixed-size arrays are useful for processing multiple values.
fn array_example(values: [Field; 5], total: pub Field) {
let mut sum: Field = 0;
for i in 0..5 {
sum = sum + values[i];
}
assert(total == sum);
}
5. Constants Constants can be defined at the top level of a file.
// Constants
const MAX_VALUE: Field = 1000000;
fn example_with_const(input: Field, output: pub Field) {
assert(input < MAX_VALUE);
assert(output == input * 2);
}
Function Types and Signatures
Main Function
The main function is the entry point of your circuit. It defines the constraints that must be satisfied.
// Signature: parameter_name: type, or parameter_name: pub type for public parameters
fn main(private_input: Field, public_output: pub Field) {
// Circuit logic goes here
assert(public_output == private_input * 2);
}
Helper Functions You can define helper functions to organize your circuit logic.
fn square(x: Field) -> Field {
x * x
}
fn complex_circuit(input: Field, output: pub Field) {
let intermediate = square(input) + input;
let result = square(intermediate);
assert(output == result);
}
Comments and Documentation
Noir supports various commenting styles:
// Single line comment
/* Multi-line comment */
/// Documentation comment for functions
/// This function proves that the output is twice the input
fn documented_function(input: Field, output: pub Field) {
assert(output == input * 2);
}
/*
* Multi-line documentation
* This circuit proves multiplication
*/
fn multiply(a: Field, b: Field, result: pub Field) {
assert(result == a * b);
}
Core Noir Concepts
1. Public vs Private Inputs
The distinction between public and private inputs is fundamental to zero-knowledge proofs:
Private Inputs (no pub keyword):
- Known only to the prover
- Not revealed to the verifier
- Used to satisfy circuit constraints
- Represent the "witness" in ZKP terminology
Public Inputs (pub keyword):
- Visible to both prover and verifier
- Part of the statement being proven
- Used to verify that the computation was performed correctly
fn example_public_private(
secret_age: Field, // PRIVATE: only prover knows this
is_adult: bool, // PRIVATE: derived from secret_age
verification_result: pub Field // PUBLIC: verifier sees this
) {
// The prover knows the secret age and whether they're an adult
// The verifier only sees the verification result
if is_adult {
assert(verification_result == 1);
} else {
assert(verification_result == 0);
}
// Additional constraint to link secret_age to is_adult
assert(secret_age >= 18 == is_adult);
}
2. Assertions: The Foundation of Constraints
Assertions are the fundamental building blocks of circuit constraints. Each assert statement creates one or more mathematical constraints that must be satisfied.
fn constraint_examples(x: Field, y: Field, result: pub Field) {
// Basic equality constraint
assert(x + y == 5);
// Inequality constraint (actually creates two constraints in the background)
assert(x != 0);
// Complex constraint
assert(result == x * x + y * y);
// Multiple constraints combined
assert(x > 0 && y > 0); // Creates x > 0 AND y > 0 constraints
}
3. Conditionals
Noir supports conditional logic, but keep in mind that both branches of an if statement are always evaluated in the underlying constraint system. Only the result of the correct branch is used in the final constraints.
fn conditional_logic(x: Field, condition: bool, result: pub Field) {
if condition {
// Both branches are converted to constraints,
// but only the true branch affects the result
assert(result == x * x);
} else {
assert(result == x * 2);
}
}
4. Functions and Modularity
Functions allow you to break down complex circuits into manageable components:
// Helper function to compute a hash
fn simple_hash(a: Field, b: Field) -> Field {
a * 31 + b * 17 + 42
}
// Main circuit function
fn hash_verification(input_a: Field, input_b: Field, expected_hash: pub Field) {
let computed_hash = simple_hash(input_a, input_b);
assert(computed_hash == expected_hash);
}
5. Loops and Iteration
Noir supports bounded loops (with known iteration count at compile time):
fn sum_array(values: [Field; 10], expected_sum: pub Field) {
let mut total: Field = 0;
let mut i: u32 = 0;
while i < 10 {
total = total + values[i as Field];
i = i + 1;
}
assert(total == expected_sum);
}
Noir's Compilation Process
From Noir to Proof: Detailed Pipeline
The compilation process in Noir is sophisticated, involving multiple stages:
1. Parsing
- The Noir source code is parsed into an Abstract Syntax Tree (AST)
- Syntax errors are caught at this stage
- The structure of the code is validated
2. Type Checking
- All variable types are checked for consistency
- Function signatures are validated
- Field arithmetic constraints are verified
- Potential overflow/underflow conditions are checked
3. Constraint Generation
- The AST is converted to an intermediate representation focused on arithmetic constraints
- High-level operations are broken down into basic arithmetic operations (+, -, *, /)
- Control flow is converted to arithmetic constraints
4. Circuit Optimization
- Common subexpression elimination
- Constant folding and propagation
- Constraint count reduction
- Backend-specific optimizations
5. Backend Compilation
- The optimized constraints are compiled for the target backend (Plonk, Plonkup, etc.)
- Backend-specific optimizations are applied
- The final circuit representation is generated
6. Proof Generation (Optional)
- If proof generation is requested, the circuit is executed with specific inputs
- A cryptographic proof is created that attests to the circuit's correct execution
Understanding the Intermediate Representations
High-Level IR: Close to original Noir code, maintains structure Mid-Level IR: Abstracts away high-level language features Low-Level IR: Arithmetic circuit representation Backend IR: Target-specific circuit representation
Compilation Optimization Examples
// Original code
fn inefficient(x: Field, y: Field, result: pub Field) {
let temp1 = x * 2;
let temp2 = temp1 + 5;
let temp3 = temp2 * y;
let temp4 = temp3 + temp2;
assert(result == temp4);
}
// After optimization (conceptual)
fn optimized(x: Field, y: pub Field, result: pub Field) {
let expression = (x * 2 + 5) * y + (x * 2 + 5);
assert(result == expression);
}
Noir's Type System
Field Type: The Workhorse of ZKPs
The Field type is central to Noir programming. It represents elements in a finite field (usually a large prime field like BN254's scalar field).
Field Properties:
- Closed under +, -, *, / operations
- Has a large prime modulus (e.g., ~2^254 for BN254)
- Supports equality and inequality comparisons
- Efficient arithmetic operations
fn field_operations_demo(a: Field, b: Field, results: pub [Field; 4]) {
// All operations stay within the field
let sum = a + b; // Addition in the field
let difference = a - b; // Subtraction in the field
let product = a * b; // Multiplication in the field
let ratio = a / b; // Division in the field (b != 0)
assert(results[0] == sum);
assert(results[1] == difference);
assert(results[2] == product);
assert(results[3] == ratio);
}
Integer Types with Bit Constraints
Noir provides integer types with explicit bit widths, useful for range checks and bit operations:
fn integer_constraints(x: u8, y: u16, result: pub u32) {
// These types ensure values fit within specified bit ranges
// u8: 0 to 2^8 - 1 (0-255)
// u16: 0 to 2^16 - 1 (0-65535)
// etc.
let sum: u32 = x as u32 + y as u32;
assert(result == sum);
// The compiler ensures x and y are within their valid ranges
}
Boolean Type and Logical Operations
fn boolean_operations(a: bool, b: bool, result: pub bool) {
let and_result = a && b;
let or_result = a || b;
let not_a = !a;
// Complex boolean expression
let complex = (a && !b) || (!a && b); // XOR equivalent
assert(result == complex);
}
Array Types and Operations
Arrays in Noir have fixed sizes known at compile time:
const ARRAY_SIZE: u32 = 5;
fn array_operations(data: [Field; ARRAY_SIZE as Field],
operations: [bool; ARRAY_SIZE as Field],
results: pub [Field; ARRAY_SIZE as Field]) {
for i in 0..ARRAY_SIZE {
if operations[i as Field] {
// Square the value if operation is true
assert(results[i as Field] == data[i as Field] * data[i as Field]);
} else {
// Double the value if operation is false
assert(results[i as Field] == data[i as Field] * 2);
}
}
}
Noir Standard Library
Built-in Functions for Common Operations
Noir provides a standard library with functions for common ZKP operations:
Range Checks
fn range_checked_computation(value: Field, result: pub Field) {
// Ensure value fits in 16 bits (0 to 65535)
value.assert_mineq(65535);
// Perform computation
let processed = value * 3 + 42;
assert(result == processed);
}
Bit Operations
fn bit_manipulation(input: Field, mask: Field, result: pub Field) {
// Extract bits using standard library functions
let bits = std::array::field_to_bits(input, 16); // Convert to 16-bit array
// Perform bit operations
let masked = input & mask;
assert(result == masked);
}
Comparison Functions
fn comparison_example(a: Field, b: Field, max_result: pub Field) {
// Get the maximum of two values
let max_val = std::cmp::max(a, b);
assert(max_result == max_val);
// Other comparison functions available:
// std::cmp::min(a, b)
// std::cmp::clamp(value, min, max)
}
Hash Functions (Backend Dependent)
// Note: Hash functions depend on the backend and may not be available in all versions
fn hash_example(data: Field, expected_hash: pub [Field; 4]) {
// This is conceptual - actual availability depends on backend
let computed_hash = std::hash::hash_to_field([data]);
// Verify each component of the hash
for i in 0..4 {
assert(expected_hash[i] == computed_hash[i]);
}
}
Proving and Verification with Nargo
The Complete Workflow
1. Circuit Design and Implementation
# Create and edit src/main.nr
2. Compilation Check
nargo check
3. Providing Input Values
Create Prover.toml in your project directory:
# Prover.toml - inputs for proof generation
x = "42"
y = "1764" # 42 squared, for the square example circuit
4. Generating a Proof
nargo prove my_proof
# Creates proof artifacts in the target directory
5. Verifying the Proof
nargo verify my_proof
# Verifies the proof without knowing the private inputs
Advanced Nargo Features
Managing Multiple Proofs
# Generate different proofs for the same circuit with different inputs
nargo prove proof1
nargo prove proof2
# Verify specific proofs
nargo verify proof1
Using Different Input Files
# Generate proof with specific input file
nargo prove my_proof --prover-input-file custom_inputs.toml
Checking Constraints
# See how many constraints your circuit generates
nargo info
# This is important for performance optimization
Advanced Noir Features
Structs and Custom Types
Noir supports defining custom data structures:
struct Point {
x: Field,
y: Field,
}
struct Line {
start: Point,
end: Point,
}
fn line_length(line: Line, expected_length_sq: pub Field) {
let dx = line.end.x - line.start.x;
let dy = line.end.y - line.start.y;
let length_sq = dx * dx + dy * dy;
assert(length_sq == expected_length_sq);
}
Enums (in more recent versions)
enum Operation {
Add,
Multiply,
Subtract,
}
fn operation_circuit(op: Operation, a: Field, b: Field, result: pub Field) {
match op {
Operation::Add => assert(result == a + b),
Operation::Multiply => assert(result == a * b),
Operation::Subtract => assert(result == a - b),
}
}
Advanced Control Flow
Complex Conditionals
fn complex_logic(x: Field, y: Field, mode: u8, result: pub Field) {
if mode == 0 {
assert(result == x + y);
} else if mode == 1 {
assert(result == x * y);
} else if mode == 2 {
assert(result == x - y);
} else {
assert(result == x / y); // Requires y != 0
}
}
Nested Conditionals
fn nested_conditions(x: Field, y: Field, condition1: bool, condition2: bool, result: pub Field) {
if condition1 {
if condition2 {
assert(result == x * y);
} else {
assert(result == x + y);
}
} else {
if condition2 {
assert(result == x - y);
} else {
assert(result == x / y);
}
}
}
Circuit Optimization in Noir
Understanding Constraint Count
The efficiency of a ZKP circuit is largely determined by the number of constraints it contains. Each constraint adds to both proving time and the mathematical complexity of the proof.
Good Practice: Minimize Constraint Count
// Less efficient - more constraints
fn inefficient_example(a: Field, b: Field, result: pub Field) {
let temp1 = a + 5;
let temp2 = temp1 * 2;
let temp3 = temp2 + b;
assert(result == temp3);
}
// More efficient - fewer constraints
fn efficient_example(a: Field, b: pub Field, result: pub Field) {
let calculation = (a + 5) * 2 + b;
assert(result == calculation);
}
Loop Optimization
// Inefficient: each iteration creates new constraints
fn inefficient_sum(values: [Field; 100], total: pub Field) {
let mut sum: Field = 0;
for i in 0..100 {
sum = sum + values[i];
}
assert(total == sum);
}
// More efficient: minimize operations inside loops where possible
fn efficient_sum(values: [Field; 100], total: pub Field) {
let mut sum: Field = 0;
for i in 0..100 {
sum += values[i]; // Compound assignment is more efficient
}
assert(total == sum);
}
Function Inlining
Noir automatically inlines simple functions, but you can influence this:
// Simple function that will likely be inlined
fn square(x: Field) -> Field {
x * x
}
// More complex function - compiler decides whether to inline
fn complex_computation(x: Field) -> Field {
let squared = square(x);
let cubed = squared * x;
cubed + x + 1
}
Debugging and Troubleshooting
Common Compilation Errors
Error: Unconstrained Variable
// This will cause an error - result is never constrained!
fn wrong_example(input: Field, result: pub Field) {
let intermediate = input * 2;
// result is not connected to any computation!
}
Corrected version:
fn correct_example(input: Field, result: pub Field) {
let intermediate = input * 2;
assert(result == intermediate); // Now result is properly constrained!
}
Error: Constraint Not Satisfiable
// This will fail if the inputs don't satisfy the constraints
fn impossible_constraints(x: Field, y: Field, result: pub Field) {
assert(x == 5); // If x != 5, this fails
assert(y == 10); // If y != 10, this fails
assert(result == x + y);
}
Debugging Strategies
1. Check Your Prover.toml Ensure input values satisfy all circuit constraints:
# Correct inputs for the circuit
x = "5"
y = "10"
result = "15" # 5 + 10 = 15
2. Use nargo check Frequently Catch errors early in the development process.
3. Start Simple Begin with basic circuits and gradually add complexity.
4. Test with Known Values Verify your circuit works with simple, predictable inputs first.
Error Messages and Solutions
"Failed to satisfy all constraints"
- Cause: Input values don't satisfy the circuit constraints
- Solution: Check your Prover.toml values against the circuit logic
"Type mismatch"
- Cause: Using wrong types in operations
- Solution: Verify all variable types match expected operations
"Array out of bounds"
- Cause: Accessing array elements beyond the declared size
- Solution: Use proper loop bounds (0 to array_length - 1)
Real-World Noir Applications
1. Private Voting Systems
struct Vote {
candidate_id: Field,
vote_weight: Field,
nonce: Field, // To prevent replay attacks
}
fn private_vote(vote: Vote, total_votes: pub [Field; 5]) {
// Verify candidate ID is valid (0-4 for 5 candidates)
vote.candidate_id.assert_mineq(4);
// Verify vote weight is valid
vote.vote_weight.assert_mineq(1); // For simple one-person-one-vote
// Add this vote to the running total
// This would be part of a larger voting aggregation system
assert(total_votes[vote.candidate_id as u32] >= vote.vote_weight);
}
2. Private Financial Verification
struct BalanceProof {
private_balance: Field,
min_balance: Field,
public_commitment: pub Field,
}
fn minimum_balance_proof(proof: BalanceProof) {
// Prove that balance is at least the minimum without revealing exact balance
assert(proof.private_balance >= proof.min_balance);
// Prove knowledge of the private balance through commitment
let expected_commitment = proof.private_balance * 17 + proof.min_balance * 23;
assert(proof.public_commitment == expected_commitment);
}
3. Supply Chain Provenance
struct ProductProvenance {
product_id: Field,
origin_hash: Field,
current_hash: Field,
transfer_sequence: [Field; 10], // Track 10 transfer steps
verification_key: pub Field,
}
fn provenance_verification(proof: ProductProvenance) {
// Verify the product chain of custody
let mut computed_hash = proof.origin_hash;
for i in 0..10 {
// Each transfer updates the hash
computed_hash = computed_hash * 31 + proof.transfer_sequence[i];
}
assert(computed_hash == proof.current_hash);
// Verify ownership of verification key
let key_check = proof.current_hash * 13 + proof.product_id * 7;
assert(key_check == proof.verification_key);
}
Visual Representations and Diagrams
Diagram 1: Noir Compilation Pipeline
Noir Source Code (.nr files)
|
v
Parsing (AST Generation)
|
v
Type Checking & Validation
|
v
Constraint Generation
|
v
Circuit Optimization
|
v
Backend Compilation
|
v
Executable Circuit
|
v
Proving/Verification
Diagram 2: Public vs Private Data Flow
Prover (Private Input) Circuit (Constraints) Verifier (Public Output)
| | |
|--- Private: secret_age (25) -------->| |
| |--- age >= 18 = true ------->|--- Public: is_adult (1)
|--- Private: secret_key (42) -------->| |
| |--- valid_key = true ------->|--- Public: access_granted (1)
| | |
Diagram 3: Noir Project Structure
Project Root/
├── src/
│ └── main.nr (main circuit code)
├── Nargo.toml (project config)
├── Prover.toml (input values)
├── target/ (compiled artifacts)
│ ├── proofs/ (generated proofs)
│ ├── circuits/ (compiled circuits)
│ └── ...
└── lib/ (if using libraries)
Knowledge Check
Consider these questions before continuing:
- What is the difference between public (
pub) and private inputs in Noir? (Hint: Think about who sees what) - How do assertions work in Noir, and what role do they play in ZKP circuits?
- What are the main steps in Noir's compilation process?
- Why is it important to consider constraint count when optimizing Noir circuits?
- What types of errors might you encounter when writing Noir circuits, and how can you debug them?
Summary
In this module, you've learned about Noir, the domain-specific language for ZKP development:
- The philosophy and design goals behind Noir, emphasizing accessibility and safety
- How to set up and use the Noir development environment with Nargo
- Core syntax including data types, functions, and control structures
- The compilation process from Noir to ZKP circuits, including optimization stages
- Advanced features like structs, enums, and complex control flow
- Circuit optimization techniques for performance
- Debugging strategies for common issues
- Real-world applications of Noir circuits
Noir significantly lowers the barrier to entry for ZKP development by providing familiar syntax while handling the mathematical complexity under the hood. The language is designed specifically for ZKP development, with built-in safety features and optimization capabilities.
In the next module, we'll put this knowledge into practice by writing our first proof circuits in Noir, starting with basic arithmetic operations and gradually building to more complex privacy-preserving computations.
Enhanced Exercises with Solutions
Exercise 4.1: Environment Setup
Install Noir on your system following the instructions provided Solution: Follow the installation steps with
curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install_noirup.sh | bash, thennoirup -v latest, and verify withnargo --version.Create a new Noir project and run through the basic commands Solution:
nargo new test_project && cd test_project && nargo check && nargo prove && nargo verifyDocument any issues you encounter and how you resolved them Solution: Common issues include Rust toolchain conflicts, permission issues with installation script, or network problems. Solutions typically involve ensuring proper permissions, having internet connectivity, or updating system packages.
Exercise 4.2: Basic Syntax Practice
Write a Noir circuit that proves knowledge of the factors of 143 (11 * 13) Solution:
fn main(factor1: Field, factor2: Field, public_product: pub Field) { assert(factor1 * factor2 == 143); assert(public_product == 143); }Prover.toml:
factor1 = "11",factor2 = "13",public_product = "143"Create a range check circuit that verifies a value is between 1 and 100 Solution:
fn main(secret_value: Field, dummy: pub Field) { // Check lower bound: secret_value >= 1 assert(secret_value >= 1); // Check upper bound: secret_value <= 100 secret_value.assert_mineq(100); assert(dummy == 0); }Implement a simple conditional circuit that squares a number if positive, doubles it if negative Solution:
fn main(input: Field, result: pub Field) { // Check if positive or negative using a boolean flag let is_positive = input >= 0; if is_positive { assert(result == input * input); } else { assert(result == input * 2); } }
Exercise 4.3: Understanding Compilation
Explain each step of Noir's compilation process in your own words Solution:
- Parsing: Converting source code to abstract syntax tree
- Type Checking: Ensuring type safety and semantic validity
- Constraint Generation: Converting high-level code to arithmetic constraints
- Optimization: Reducing constraint count and improving efficiency
- Backend Compilation: Converting to target proof system format
Why is optimization important in the compilation pipeline? Solution: Fewer constraints mean faster proving time and smaller proof sizes, making the ZKP more practical for real-world applications.
How do backends affect the final proof system used? Solution: Different backends (Plonk, Plonkup, etc.) generate different types of proofs with different performance characteristics and features (like lookup tables).
Exercise 4.4: Advanced Noir Concepts
Create a Noir circuit that implements a simple private auction where bidders can prove they have bid above a minimum without revealing their exact bid amount.
Solution:
fn private_auction_bid(
private_bid: Field, // Bidder's private bid amount
min_bid: Field, // Minimum allowed bid (public)
max_bid: Field, // Maximum expected bid (for range check)
bid_commitment: pub Field // Commitment to the bid for verification
) {
// Verify the bid is within acceptable range
assert(private_bid >= min_bid); // Bid must meet minimum
private_bid.assert_mineq(max_bid); // Bid must not exceed maximum
// Create a commitment to show knowledge of the bid
// This allows verification without revealing the exact amount
let computed_commitment = private_bid * 17 + min_bid * 23 + max_bid * 7;
assert(bid_commitment == computed_commitment);
}
This circuit allows a bidder to prove their bid meets the minimum requirement without revealing the exact bid amount, maintaining privacy while ensuring rule compliance.