From f7df37f04095a60cd09412ea76712c84b1a4bc47 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Thu, 4 Jul 2024 14:07:04 +0100 Subject: [PATCH 01/14] docs move codegen doc amongst alongside other tooling references --- .../docs/{getting_started/tooling => reference}/noir_codegen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/docs/{getting_started/tooling => reference}/noir_codegen.md (97%) diff --git a/docs/docs/getting_started/tooling/noir_codegen.md b/docs/docs/reference/noir_codegen.md similarity index 97% rename from docs/docs/getting_started/tooling/noir_codegen.md rename to docs/docs/reference/noir_codegen.md index f7505bef7ab..db8f07dc22e 100644 --- a/docs/docs/getting_started/tooling/noir_codegen.md +++ b/docs/docs/reference/noir_codegen.md @@ -33,7 +33,7 @@ yarn add @noir-lang/noir_codegen -D ``` ### Nargo library -Make sure you have Nargo, v0.25.0 or greater, installed. If you don't, follow the [installation guide](../installation/index.md). +Make sure you have Nargo, v0.25.0 or greater, installed. If you don't, follow the [installation guide](../getting_started/installation/index.md). If you're in a new project, make a `circuits` folder and create a new Noir library: From c4f4a284df92de611c65b8ec796137d88e8438eb Mon Sep 17 00:00:00 2001 From: James Zaki Date: Fri, 5 Jul 2024 17:31:11 +0100 Subject: [PATCH 02/14] Add Writing Noir doc --- docs/docs/getting_started/writing_noir.md | 210 ++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 docs/docs/getting_started/writing_noir.md diff --git a/docs/docs/getting_started/writing_noir.md b/docs/docs/getting_started/writing_noir.md new file mode 100644 index 00000000000..5f7bde8a77a --- /dev/null +++ b/docs/docs/getting_started/writing_noir.md @@ -0,0 +1,210 @@ +--- +title: Writing Noir +description: Understand new considerations when writing Noir +keywords: [Noir, programming, rust] +tags: [Optimisation] +sidebar_position: 3 +--- + +# Writing Noir for fun and profit + +This article intends to set you up with a key concept essential for writing more viable applications that use zero knowledge proofs, namely around non-wasteful circuits. + +## Context - 'Efficient' is subjective + +When writing a web application for a performant computer with high-speed internet connection, writing efficient code sometimes is seen as an afterthought only if needed. Large multiplications running at the innermost of nested loops may not even be on a dev's radar. +When writing firmware for a battery-powered microcontroller, you think of cpu cycles as rations to keep within a product's power budget. + +> Code is written to create applications that perform specific tasks within specific constraints + +And these constraints differ depending on where the compiled code is execute. +### The Ethereum Virtual Machine (EVM) + +For the EVM, Solidity code is compiled into bytecode to be executed, and the gas cost of opcodes become as important as clock cycles. These gas costs were designed in to the protocol to reward machines that are executing bytecode for the EVM, so it is no surprise that a lot of the costs are roughly proportional to clock-cycles (a proxy for power consumption). Eg Addition: 3, Multiplication: 8. + +But there is a twist, the cost of writing to disk is amplified, and is a few orders of magnitude larger. Namely: 20k for allocating and writing a new value, or 5k for writing an existing value. +So whilst writing efficient code for regular computers and the EVM is mostly the same, there are some key differences like this more immediate compensation of disk writes. + +In scenarios where extremely low gas costs are required for an application to be viable/competitive, developers get into what is colloquially known as: "*gas golfing*". Finding the lowest execution cost to achieve a specific task. + +### Coding for circuits - a paradigm shift + +In zero knowledge cryptography, code is compiled to arithmetic gates called "circuits", and gate count is the significant cost. Depending on the backend this is linearly proportionate to proving time, and so from a product point this should be kept as low as possible. + +Whilst writing efficient code for web apps and solidity has a few key differences, writing efficient circuits have a different set of considerations. It is a bit of a paradigm shift, like writing code for GPUs for the first time... + +Eg drawing a circle at (0, 0) of radius `r`: +- For a single CPU thread, +``` +for theta in 0..2*pi { + let x = r * cos(theta); + let y = r * sin(theta); + draw(x, y); +} // note: would do 0 - pi/2 and draw +ve/-ve x and y. +``` + +- For GPUs (simultaneous parallel calls with x, y across image), +``` +if (x^2 + y^2 = r^2) { + draw(x, y); +} +``` + +([Related](https://www.youtube.com/watch?v=-P28LKWTzrI)) + +Whilst this CPU -> GPU does not translate to circuits exactly, it is intended to exemplify the difference in intuition when coding for different machine capabilities/constraints. + +### Context Takeaway + +For those coming from a primarily web app background, this article will explain what you need to consider when writing circuits. Furthermore, for those experienced writing efficient machine code, prepare to shift what you think is efficient 😬 + +## Code re-use + +For some applications using Noir, existing code might be a convenient starting point to then proceed to optimise the gate count of. + +:::note +Many valuable functions and algorithms have been written in more established languages (C/C++), and converted to modern ones (like Rust). +::: + +Fortunately for Noir devs, when needing a particular function a rust implementation can be readily compiled into Noir with minimal changes. While the compiler does a decent amount of heavy lifting, it won't change code that has been optimized for clock-cycles into code optimized for arithmetic gates. + +## Writing efficient Noir for performant products + +The following points help refine our understanding over time. + +:::note +> A Noir program makes a statement that can be verified. +::: + +It compiles to a structure that represents the calculation, and can assert results within the calculation at any stage (via the `constrain` keyword). + +A Noir program compiles to an Abstract Circuit Intermediate Representation which is: + - A tree structure + - Leaves (inputs) are the `Field` type + - Nodes contain arithmetic operations to combine them (gates) + - The root is the final result (return value) + +### Use the `Field` type + +Since the native type of values in circuits are `Field`s, using them for variables in Noir means less gates converting them under the hood. + +:::tip +Where possible, use `Field` type for values. Using smaller value types, and bitpacking strategies, will result in MORE gates +::: + +**Note:** Need to remain mindful of overflow. Types with less bits may be used to limit the range of possible values prior to a calculation. + +### Use Arithmetic over non-arithmetic operations + +Since circuits are made of arithmetic gates, the cost of them tends to be one gate. Whereas for procedural code, they represent several clock cycles. + +Inversely, non-arithmetic operators are achieved with multipled gates, vs 1 clock cycle for procedural code. + +| (cost\op) | arithmetic
(eg `*`, `+`) | bit-wise ops
(eg `<`, `\|`, `>>`) | +| - | - | - | +| **cycles** | 10+ | 1 | +| **gates** | 1 | 10+ | + +:::tip +Preference arithmetic operators where possible. Attempting to optimise a circuit with bit-wise operations will lead to MORE gates. +::: + +### Use static over dynamic values + +Another general theme that manifests in different ways is that static reads are represented with less gates than dynamic ones. + +Reading from read-only memory (ROM) adds less gates than random-access memory (RAM), 2 vs ~3.25 due to the additional bounds checks. Arrays of fixed length (albeit used at a lower capacity), will generate less gates than dynamic storage. + +Related to this, if an index used to access an array is not known at compile time (ie unknown until run time), then ROM will be converted to RAM, expanding the gate count. + +:::tip +Use arrays and indices that are known at compile time where possible. +NB: Using `assert_constant(i);` before an index, `i`, is used in an array will give a compile error if `i` is NOT known at compile time. +::: + + +### Leverage unconstrained execution + +Constrained verification can leverage unconstrained execution. +Compute result via unconstrained function, verify result. + +Use ` if is_unconstrained() { /`, to conditionally + +#### Token transfer example - `fn sub` inefficiencies + +Iterates through (32) notes, and does nullifier and membership checks... + +Use unconstrained function to find two (largest) notes, if not enough recursively combine two notes. +"call" expensive, can use "multicall". + + +### A circuit holds all logic + +In procedural execution, the following logic: `if (a>0 && b>0) {` , will not perform the second comparison if the first one is false. Whereas a circuit will hold both paths. +Implementing this type of short-circuiting is much less efficient for circuits since it is effectively adding additional comparisons which add more gates. + +:::tip +Use bit-wise `&` or `|` to combine logical expressions efficiently. +::: + +## Advanced + +### Combine arithmetic operations + +A Noir program can be honed further by combining arithmetic operators in a way that makes the most of each gate (Width 4). + +- (katex) w_1*w_2*q_m + ... + +#### Variable as witness vs expression + +`std::as_witness` means variable is interpreted as a witness not an expression. +When used incorrecty will create **less** efficient circuits (higher gate count). + +## Rust to Noir + +A few things to do when converting Rust code to Noir: +- Early `return` in function. Use `constrain` instead. +- Reference `&` operator. Remove, will be value copy. +- Type `usize`. Use types `u8`, `u32`, `u64`, ... +- `main` return must be public, `pub`. + + +## References +### Guillaume's "Cryptdoku" [video](https://www.youtube.com/watch?v=MrQyzuogxgg) (June'23): + +Notes from the video: +- ROM lookup is cheaper than RAM lookup + - 2 gates for ROM vs 3.25 for RAM due to range check (from slack) +- Fixed indices vs dynamic +- Arithmetic operations over bitwise logic and comparisons +- Fields better +- Compilation from code to circuit (constraints in a field), all values must be a field in the circuit +- If summing fields, risk overflow, can sum n-bit numbers with n and number of additions guaranteed not to overflow. But priority is use fields wherever possible. + +1527 ACIR gates generated for solving sudoku + +Q: Any existing guidance in changing a Rust program to Noir? +A: No. + + +### Tom's Tips (Jun'24): + +``` +- Try to avoid mutating arrays at unknown (until runtime) indices as this turns ROM into RAM which is more expensive. In this case it's best to construct the final output in an unconstrained function and then assert that it's correct. +- Unconstrained gud, so also useful in non-array settings if you can prove the result cheaply once you know it. +- Bitwise operations are bad and should be avoided if possible (notable as devs tend to use bitwise ops in an attempt to optimise their code) +- We do as much compile-time execution as we can so calling "expensive" functions with constant arguments isn't a major concern and developers shouldn't feel the need to create hardcoded constants. +``` ++ Tip from Jake: `One way to avoid accessing arrays with runtime indices is putting a `assert_constant(my_index);` on the line before an array access so that you get a compile-time error if it is not constant` +### Idiomatic Noir (from vlayer) + + [aritcle](https://www.vlayer.xyz/blog/idiomatic-noir-part-1-collections) + +### Tips and Tricks from Zac: + +1. Compute and Measure +2. If loops are producing non-linear costs, investigate! +3. Compute in unconstrained, validate in constrained functions + - Take care to not create invalid constraints +4. Optimise for happy path +5. If statements in loops where predicate not known at compile time are dangerous! From e5baf924c1a691529d82d57217c5186ec2db5e7f Mon Sep 17 00:00:00 2001 From: James Zaki Date: Mon, 8 Jul 2024 17:28:16 +0100 Subject: [PATCH 03/14] Update doc writing noir --- docs/docs/getting_started/writing_noir.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/docs/getting_started/writing_noir.md b/docs/docs/getting_started/writing_noir.md index 5f7bde8a77a..74815374d92 100644 --- a/docs/docs/getting_started/writing_noir.md +++ b/docs/docs/getting_started/writing_noir.md @@ -66,7 +66,7 @@ For some applications using Noir, existing code might be a convenient starting p Many valuable functions and algorithms have been written in more established languages (C/C++), and converted to modern ones (like Rust). ::: -Fortunately for Noir devs, when needing a particular function a rust implementation can be readily compiled into Noir with minimal changes. While the compiler does a decent amount of heavy lifting, it won't change code that has been optimized for clock-cycles into code optimized for arithmetic gates. +Fortunately for Noir devs, when needing a particular function a rust implementation can be readily compiled into Noir with some key changes. While the compiler does a decent amount of optimisations, it won't be able to change code that has been optimized for clock-cycles into code optimized for arithmetic gates. ## Writing efficient Noir for performant products @@ -84,6 +84,11 @@ A Noir program compiles to an Abstract Circuit Intermediate Representation which - Nodes contain arithmetic operations to combine them (gates) - The root is the final result (return value) +:::tip +The command `nargo info` shows the programs circuit size, and is useful to compare the value of changes made. +Advanced: You can dig deeper and use the `--print-acir` param to take a closer look at individual gates too. +::: + ### Use the `Field` type Since the native type of values in circuits are `Field`s, using them for variables in Noir means less gates converting them under the hood. @@ -122,13 +127,12 @@ Use arrays and indices that are known at compile time where possible. NB: Using `assert_constant(i);` before an index, `i`, is used in an array will give a compile error if `i` is NOT known at compile time. ::: - ### Leverage unconstrained execution Constrained verification can leverage unconstrained execution. Compute result via unconstrained function, verify result. -Use ` if is_unconstrained() { /`, to conditionally +Use ` if is_unconstrained() { /`, to conditionally execute code if being called in an unconstrained vs constrained way. #### Token transfer example - `fn sub` inefficiencies @@ -137,7 +141,6 @@ Iterates through (32) notes, and does nullifier and membership checks... Use unconstrained function to find two (largest) notes, if not enough recursively combine two notes. "call" expensive, can use "multicall". - ### A circuit holds all logic In procedural execution, the following logic: `if (a>0 && b>0) {` , will not perform the second comparison if the first one is false. Whereas a circuit will hold both paths. @@ -151,9 +154,14 @@ Use bit-wise `&` or `|` to combine logical expressions efficiently. ### Combine arithmetic operations -A Noir program can be honed further by combining arithmetic operators in a way that makes the most of each gate (Width 4). +A Noir program can be honed further by combining arithmetic operators in a way that makes the most of each constraint of the backend. This is in scenarios where the backend might not be doing this perfectly. + +Eg Barretenberg backend (current default for Noir) is a width-4 PLONKish constraint system +$ w_1*w_2*q_m + w_1*q_1 + w_2*q_2 + w_3*q_3 + w_4*q_4 + q_c $ + +Here we see there is one occurance of witness 1 and 2 ($w_1$, $w_2$) being multiplied together, with addition to witnesses 1-4 ($w_1$ .. $w_4$) multiplied by 4 corresponding circuit constants ($q_1$ .. $q_4$) (plus a final circuit constant, $q_c$). -- (katex) w_1*w_2*q_m + ... +Use `nargo info --print-acir`, to inspect the constraints, and it may present opportunities to amend the order of operations and reduce the number of constraints. #### Variable as witness vs expression From c0013e06d25a55b86367bda7143b0dc99f4ca100 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Tue, 9 Jul 2024 15:55:27 +0100 Subject: [PATCH 04/14] Update writing noir doc --- docs/docs/getting_started/writing_noir.md | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/docs/docs/getting_started/writing_noir.md b/docs/docs/getting_started/writing_noir.md index 74815374d92..e7e6a7c71f0 100644 --- a/docs/docs/getting_started/writing_noir.md +++ b/docs/docs/getting_started/writing_noir.md @@ -68,6 +68,15 @@ Many valuable functions and algorithms have been written in more established lan Fortunately for Noir devs, when needing a particular function a rust implementation can be readily compiled into Noir with some key changes. While the compiler does a decent amount of optimisations, it won't be able to change code that has been optimized for clock-cycles into code optimized for arithmetic gates. +A few things to do when converting Rust code to Noir: +- `println!` is not a macro, use `println` function (same for `assert_eq`) +- No early `return` in function. Use constrain via assertion instead +- No reference `&` operator. Remove, will be value copy +- No type `usize`. Use types `u8`, `u32`, `u64`, ... +- `main` return must be public, `pub` +- No `const`, use `global` +- Noir's LSP is your friend, so error message should be informative enough to resolve syntax issues. + ## Writing efficient Noir for performant products The following points help refine our understanding over time. @@ -152,6 +161,8 @@ Use bit-wise `&` or `|` to combine logical expressions efficiently. ## Advanced +Unless you're well into the depth of gate optimisation, this advanced section can be ignored. + ### Combine arithmetic operations A Noir program can be honed further by combining arithmetic operators in a way that makes the most of each constraint of the backend. This is in scenarios where the backend might not be doing this perfectly. @@ -168,15 +179,6 @@ Use `nargo info --print-acir`, to inspect the constraints, and it may present op `std::as_witness` means variable is interpreted as a witness not an expression. When used incorrecty will create **less** efficient circuits (higher gate count). -## Rust to Noir - -A few things to do when converting Rust code to Noir: -- Early `return` in function. Use `constrain` instead. -- Reference `&` operator. Remove, will be value copy. -- Type `usize`. Use types `u8`, `u32`, `u64`, ... -- `main` return must be public, `pub`. - - ## References ### Guillaume's "Cryptdoku" [video](https://www.youtube.com/watch?v=MrQyzuogxgg) (June'23): @@ -202,8 +204,11 @@ A: No. - Unconstrained gud, so also useful in non-array settings if you can prove the result cheaply once you know it. - Bitwise operations are bad and should be avoided if possible (notable as devs tend to use bitwise ops in an attempt to optimise their code) - We do as much compile-time execution as we can so calling "expensive" functions with constant arguments isn't a major concern and developers shouldn't feel the need to create hardcoded constants. + ``` + + Tip from Jake: `One way to avoid accessing arrays with runtime indices is putting a `assert_constant(my_index);` on the line before an array access so that you get a compile-time error if it is not constant` + ### Idiomatic Noir (from vlayer) [aritcle](https://www.vlayer.xyz/blog/idiomatic-noir-part-1-collections) @@ -214,5 +219,5 @@ A: No. 2. If loops are producing non-linear costs, investigate! 3. Compute in unconstrained, validate in constrained functions - Take care to not create invalid constraints -4. Optimise for happy path +4. Optimise for happy path using unconstrained functions 5. If statements in loops where predicate not known at compile time are dangerous! From fbd90e74f2986b769cd9e6ee27723ca1b0878735 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Tue, 9 Jul 2024 16:48:57 +0100 Subject: [PATCH 05/14] docs final tweaks --- .../writing_noir.md => explainers/explainer-writing-noir.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename docs/docs/{getting_started/writing_noir.md => explainers/explainer-writing-noir.md} (99%) diff --git a/docs/docs/getting_started/writing_noir.md b/docs/docs/explainers/explainer-writing-noir.md similarity index 99% rename from docs/docs/getting_started/writing_noir.md rename to docs/docs/explainers/explainer-writing-noir.md index e7e6a7c71f0..6be764fc896 100644 --- a/docs/docs/getting_started/writing_noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -168,13 +168,13 @@ Unless you're well into the depth of gate optimisation, this advanced section ca A Noir program can be honed further by combining arithmetic operators in a way that makes the most of each constraint of the backend. This is in scenarios where the backend might not be doing this perfectly. Eg Barretenberg backend (current default for Noir) is a width-4 PLONKish constraint system -$ w_1*w_2*q_m + w_1*q_1 + w_2*q_2 + w_3*q_3 + w_4*q_4 + q_c $ +$ w_1*w_2*q_m + w_1*q_1 + w_2*q_2 + w_3*q_3 + w_4*q_4 + q_c = 0 $ Here we see there is one occurance of witness 1 and 2 ($w_1$, $w_2$) being multiplied together, with addition to witnesses 1-4 ($w_1$ .. $w_4$) multiplied by 4 corresponding circuit constants ($q_1$ .. $q_4$) (plus a final circuit constant, $q_c$). Use `nargo info --print-acir`, to inspect the constraints, and it may present opportunities to amend the order of operations and reduce the number of constraints. -#### Variable as witness vs expression +### Variable as witness vs expression `std::as_witness` means variable is interpreted as a witness not an expression. When used incorrecty will create **less** efficient circuits (higher gate count). From a5e4b3b83620c0f699c2c87c8206cadbbbd6c0a1 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Tue, 9 Jul 2024 16:57:51 +0100 Subject: [PATCH 06/14] Move to first explainer --- docs/docs/explainers/explainer-writing-noir.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index 6be764fc896..47d6c645337 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -3,7 +3,7 @@ title: Writing Noir description: Understand new considerations when writing Noir keywords: [Noir, programming, rust] tags: [Optimisation] -sidebar_position: 3 +sidebar_position: 0 --- # Writing Noir for fun and profit From 6153c830d2b59d8718a3ed1ac29fe8640d179e11 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Wed, 10 Jul 2024 10:41:16 +0100 Subject: [PATCH 07/14] Fix typos --- docs/docs/explainers/explainer-writing-noir.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index 47d6c645337..c0121764325 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -2,7 +2,7 @@ title: Writing Noir description: Understand new considerations when writing Noir keywords: [Noir, programming, rust] -tags: [Optimisation] +tags: [Optimization] sidebar_position: 0 --- @@ -60,13 +60,13 @@ For those coming from a primarily web app background, this article will explain ## Code re-use -For some applications using Noir, existing code might be a convenient starting point to then proceed to optimise the gate count of. +For some applications using Noir, existing code might be a convenient starting point to then proceed to optimize the gate count of. :::note Many valuable functions and algorithms have been written in more established languages (C/C++), and converted to modern ones (like Rust). ::: -Fortunately for Noir devs, when needing a particular function a rust implementation can be readily compiled into Noir with some key changes. While the compiler does a decent amount of optimisations, it won't be able to change code that has been optimized for clock-cycles into code optimized for arithmetic gates. +Fortunately for Noir developers, when needing a particular function a rust implementation can be readily compiled into Noir with some key changes. While the compiler does a decent amount of optimizations, it won't be able to change code that has been optimized for clock-cycles into code optimized for arithmetic gates. A few things to do when converting Rust code to Noir: - `println!` is not a macro, use `println` function (same for `assert_eq`) @@ -103,7 +103,7 @@ Advanced: You can dig deeper and use the `--print-acir` param to take a closer l Since the native type of values in circuits are `Field`s, using them for variables in Noir means less gates converting them under the hood. :::tip -Where possible, use `Field` type for values. Using smaller value types, and bitpacking strategies, will result in MORE gates +Where possible, use `Field` type for values. Using smaller value types, and bit-packing strategies, will result in MORE gates ::: **Note:** Need to remain mindful of overflow. Types with less bits may be used to limit the range of possible values prior to a calculation. @@ -112,7 +112,7 @@ Where possible, use `Field` type for values. Using smaller value types, and bitp Since circuits are made of arithmetic gates, the cost of them tends to be one gate. Whereas for procedural code, they represent several clock cycles. -Inversely, non-arithmetic operators are achieved with multipled gates, vs 1 clock cycle for procedural code. +Inversely, non-arithmetic operators are achieved with multiple gates, vs 1 clock cycle for procedural code. | (cost\op) | arithmetic
(eg `*`, `+`) | bit-wise ops
(eg `<`, `\|`, `>>`) | | - | - | - | @@ -120,7 +120,7 @@ Inversely, non-arithmetic operators are achieved with multipled gates, vs 1 cloc | **gates** | 1 | 10+ | :::tip -Preference arithmetic operators where possible. Attempting to optimise a circuit with bit-wise operations will lead to MORE gates. +Preference arithmetic operators where possible. Attempting to optimize a circuit with bit-wise operations will lead to MORE gates. ::: ### Use static over dynamic values @@ -161,7 +161,7 @@ Use bit-wise `&` or `|` to combine logical expressions efficiently. ## Advanced -Unless you're well into the depth of gate optimisation, this advanced section can be ignored. +Unless you're well into the depth of gate optimization, this advanced section can be ignored. ### Combine arithmetic operations @@ -170,14 +170,14 @@ A Noir program can be honed further by combining arithmetic operators in a way t Eg Barretenberg backend (current default for Noir) is a width-4 PLONKish constraint system $ w_1*w_2*q_m + w_1*q_1 + w_2*q_2 + w_3*q_3 + w_4*q_4 + q_c = 0 $ -Here we see there is one occurance of witness 1 and 2 ($w_1$, $w_2$) being multiplied together, with addition to witnesses 1-4 ($w_1$ .. $w_4$) multiplied by 4 corresponding circuit constants ($q_1$ .. $q_4$) (plus a final circuit constant, $q_c$). +Here we see there is one occurrence of witness 1 and 2 ($w_1$, $w_2$) being multiplied together, with addition to witnesses 1-4 ($w_1$ .. $w_4$) multiplied by 4 corresponding circuit constants ($q_1$ .. $q_4$) (plus a final circuit constant, $q_c$). Use `nargo info --print-acir`, to inspect the constraints, and it may present opportunities to amend the order of operations and reduce the number of constraints. ### Variable as witness vs expression `std::as_witness` means variable is interpreted as a witness not an expression. -When used incorrecty will create **less** efficient circuits (higher gate count). +When used incorrectly will create **less** efficient circuits (higher gate count). ## References ### Guillaume's "Cryptdoku" [video](https://www.youtube.com/watch?v=MrQyzuogxgg) (June'23): From 4cb3c00de7cb29dde80a65d570e281858417b70e Mon Sep 17 00:00:00 2001 From: James Zaki Date: Wed, 10 Jul 2024 11:00:09 +0100 Subject: [PATCH 08/14] consolidate refs --- .../docs/explainers/explainer-writing-noir.md | 44 ++----------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index c0121764325..4030e7e5219 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -180,44 +180,6 @@ Use `nargo info --print-acir`, to inspect the constraints, and it may present op When used incorrectly will create **less** efficient circuits (higher gate count). ## References -### Guillaume's "Cryptdoku" [video](https://www.youtube.com/watch?v=MrQyzuogxgg) (June'23): - -Notes from the video: -- ROM lookup is cheaper than RAM lookup - - 2 gates for ROM vs 3.25 for RAM due to range check (from slack) -- Fixed indices vs dynamic -- Arithmetic operations over bitwise logic and comparisons -- Fields better -- Compilation from code to circuit (constraints in a field), all values must be a field in the circuit -- If summing fields, risk overflow, can sum n-bit numbers with n and number of additions guaranteed not to overflow. But priority is use fields wherever possible. - -1527 ACIR gates generated for solving sudoku - -Q: Any existing guidance in changing a Rust program to Noir? -A: No. - - -### Tom's Tips (Jun'24): - -``` -- Try to avoid mutating arrays at unknown (until runtime) indices as this turns ROM into RAM which is more expensive. In this case it's best to construct the final output in an unconstrained function and then assert that it's correct. -- Unconstrained gud, so also useful in non-array settings if you can prove the result cheaply once you know it. -- Bitwise operations are bad and should be avoided if possible (notable as devs tend to use bitwise ops in an attempt to optimise their code) -- We do as much compile-time execution as we can so calling "expensive" functions with constant arguments isn't a major concern and developers shouldn't feel the need to create hardcoded constants. - -``` - -+ Tip from Jake: `One way to avoid accessing arrays with runtime indices is putting a `assert_constant(my_index);` on the line before an array access so that you get a compile-time error if it is not constant` - -### Idiomatic Noir (from vlayer) - - [aritcle](https://www.vlayer.xyz/blog/idiomatic-noir-part-1-collections) - -### Tips and Tricks from Zac: - -1. Compute and Measure -2. If loops are producing non-linear costs, investigate! -3. Compute in unconstrained, validate in constrained functions - - Take care to not create invalid constraints -4. Optimise for happy path using unconstrained functions -5. If statements in loops where predicate not known at compile time are dangerous! +- Guillaume's "Cryptdoku" [video](https://www.youtube.com/watch?v=MrQyzuogxgg) (Jun'23) +- Tips from Tom, Jake and Zac. +- [Idiomatic Noir](https://www.vlayer.xyz/blog/idiomatic-noir-part-1-collections) (from vlayer) From cdeee389ac03e628db580cd722b95107888484de Mon Sep 17 00:00:00 2001 From: James Zaki Date: Wed, 10 Jul 2024 11:58:38 +0100 Subject: [PATCH 09/14] fix cspell warnings that error ci --- cspell.json | 2 ++ docs/docs/explainers/explainer-writing-noir.md | 11 ++--------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/cspell.json b/cspell.json index 2a9bfb4b544..46ed4d5f071 100644 --- a/cspell.json +++ b/cspell.json @@ -126,6 +126,7 @@ "memset", "merkle", "metas", + "microcontroller", "minreq", "monomorphization", "monomorphize", @@ -135,6 +136,7 @@ "monomorphizing", "montcurve", "MSRV", + "multicall", "nand", "nargo", "neovim", diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index 4030e7e5219..3792f2c2543 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -143,13 +143,6 @@ Compute result via unconstrained function, verify result. Use ` if is_unconstrained() { /`, to conditionally execute code if being called in an unconstrained vs constrained way. -#### Token transfer example - `fn sub` inefficiencies - -Iterates through (32) notes, and does nullifier and membership checks... - -Use unconstrained function to find two (largest) notes, if not enough recursively combine two notes. -"call" expensive, can use "multicall". - ### A circuit holds all logic In procedural execution, the following logic: `if (a>0 && b>0) {` , will not perform the second comparison if the first one is false. Whereas a circuit will hold both paths. @@ -180,6 +173,6 @@ Use `nargo info --print-acir`, to inspect the constraints, and it may present op When used incorrectly will create **less** efficient circuits (higher gate count). ## References -- Guillaume's "Cryptdoku" [video](https://www.youtube.com/watch?v=MrQyzuogxgg) (Jun'23) +- Guillaume's ["`Cryptdoku`" talk](https://www.youtube.com/watch?v=MrQyzuogxgg) (Jun'23) - Tips from Tom, Jake and Zac. -- [Idiomatic Noir](https://www.vlayer.xyz/blog/idiomatic-noir-part-1-collections) (from vlayer) +- [Idiomatic Noir](https://www.vlayer.xyz/blog/idiomatic-noir-part-1-collections) blog post From dafecfe8b7bacd9ed40bb45c3a246dd2adec2734 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Wed, 10 Jul 2024 12:22:28 +0100 Subject: [PATCH 10/14] add cspell word for folder --- docs/docs/explainers/cspell.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/docs/explainers/cspell.json diff --git a/docs/docs/explainers/cspell.json b/docs/docs/explainers/cspell.json new file mode 100644 index 00000000000..c60b0a597b1 --- /dev/null +++ b/docs/docs/explainers/cspell.json @@ -0,0 +1,5 @@ +{ + "words": [ + "Cryptdoku" + ] +} From 01853db0b9f1bb68cf1507e5028928ba148e1b8d Mon Sep 17 00:00:00 2001 From: James Zaki Date: Thu, 25 Jul 2024 14:37:34 +0100 Subject: [PATCH 11/14] Apply suggestions from code review Thanks Josh, Savio. Co-authored-by: josh crites Co-authored-by: Savio <72797635+Savio-Sou@users.noreply.github.com> --- .../docs/explainers/explainer-writing-noir.md | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index 3792f2c2543..4c47701773e 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -8,7 +8,7 @@ sidebar_position: 0 # Writing Noir for fun and profit -This article intends to set you up with a key concept essential for writing more viable applications that use zero knowledge proofs, namely around non-wasteful circuits. +This article intends to set you up with key concepts essential for writing more viable applications that use zero knowledge proofs, namely around efficient circuits. ## Context - 'Efficient' is subjective @@ -29,11 +29,11 @@ In scenarios where extremely low gas costs are required for an application to be ### Coding for circuits - a paradigm shift -In zero knowledge cryptography, code is compiled to arithmetic gates called "circuits", and gate count is the significant cost. Depending on the backend this is linearly proportionate to proving time, and so from a product point this should be kept as low as possible. +In zero knowledge cryptography, code is compiled to "circuits" consisting of arithmetic gates, and gate count is the significant cost. Depending on the proving system this is linearly proportionate to proving time, and so from a product point this should be kept as low as possible. -Whilst writing efficient code for web apps and solidity has a few key differences, writing efficient circuits have a different set of considerations. It is a bit of a paradigm shift, like writing code for GPUs for the first time... +Whilst writing efficient code for web apps and Solidity has a few key differences, writing efficient circuits have a different set of considerations. It is a bit of a paradigm shift, like writing code for GPUs for the first time... -Eg drawing a circle at (0, 0) of radius `r`: +For example, drawing a circle at (0, 0) of radius `r`: - For a single CPU thread, ``` for theta in 0..2*pi { @@ -58,7 +58,7 @@ Whilst this CPU -> GPU does not translate to circuits exactly, it is intended to For those coming from a primarily web app background, this article will explain what you need to consider when writing circuits. Furthermore, for those experienced writing efficient machine code, prepare to shift what you think is efficient 😬 -## Code re-use +## Translating from Rust For some applications using Noir, existing code might be a convenient starting point to then proceed to optimize the gate count of. @@ -66,7 +66,7 @@ For some applications using Noir, existing code might be a convenient starting p Many valuable functions and algorithms have been written in more established languages (C/C++), and converted to modern ones (like Rust). ::: -Fortunately for Noir developers, when needing a particular function a rust implementation can be readily compiled into Noir with some key changes. While the compiler does a decent amount of optimizations, it won't be able to change code that has been optimized for clock-cycles into code optimized for arithmetic gates. +Fortunately for Noir developers, when needing a particular function a Rust implementation can be readily compiled into Noir with some key changes. While the compiler does a decent amount of optimizations, it won't be able to change code that has been optimized for clock-cycles into code optimized for arithmetic gates. A few things to do when converting Rust code to Noir: - `println!` is not a macro, use `println` function (same for `assert_eq`) @@ -82,7 +82,7 @@ A few things to do when converting Rust code to Noir: The following points help refine our understanding over time. :::note -> A Noir program makes a statement that can be verified. +A Noir program makes a statement that can be verified. ::: It compiles to a structure that represents the calculation, and can assert results within the calculation at any stage (via the `constrain` keyword). @@ -110,7 +110,7 @@ Where possible, use `Field` type for values. Using smaller value types, and bit- ### Use Arithmetic over non-arithmetic operations -Since circuits are made of arithmetic gates, the cost of them tends to be one gate. Whereas for procedural code, they represent several clock cycles. +Since circuits are made of arithmetic gates, the cost of arithmetic operations tends to be one gate. Whereas for procedural code, they represent several clock cycles. Inversely, non-arithmetic operators are achieved with multiple gates, vs 1 clock cycle for procedural code. @@ -119,9 +119,11 @@ Inversely, non-arithmetic operators are achieved with multiple gates, vs 1 clock | **cycles** | 10+ | 1 | | **gates** | 1 | 10+ | -:::tip -Preference arithmetic operators where possible. Attempting to optimize a circuit with bit-wise operations will lead to MORE gates. -::: +Bit-wise operations (e.g. bit shifts `<<` and `>>`), albeit commonly used in general programming and especially for clock cycle optimizations, are on the contrary expensive in gates when performed within circuits. + +Translate away from bit shifts when writing constrained functions for the best performance. + +On the flip side, feel free to use bit shifts in unconstrained functions and tests if necessary, as they are executed outside of circuits and does not induce performance hits. ### Use static over dynamic values @@ -133,7 +135,7 @@ Related to this, if an index used to access an array is not known at compile tim :::tip Use arrays and indices that are known at compile time where possible. -NB: Using `assert_constant(i);` before an index, `i`, is used in an array will give a compile error if `i` is NOT known at compile time. +Using `assert_constant(i);` before an index, `i`, is used in an array will give a compile error if `i` is NOT known at compile time. ::: ### Leverage unconstrained execution @@ -158,7 +160,7 @@ Unless you're well into the depth of gate optimization, this advanced section ca ### Combine arithmetic operations -A Noir program can be honed further by combining arithmetic operators in a way that makes the most of each constraint of the backend. This is in scenarios where the backend might not be doing this perfectly. +A Noir program can be honed further by combining arithmetic operators in a way that makes the most of each constraint of the backend proving system. This is in scenarios where the backend might not be doing this perfectly. Eg Barretenberg backend (current default for Noir) is a width-4 PLONKish constraint system $ w_1*w_2*q_m + w_1*q_1 + w_2*q_2 + w_3*q_3 + w_4*q_4 + q_c = 0 $ From 5ad9efaa3f50b274d348686226780d3f256d2f85 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Thu, 25 Jul 2024 14:46:00 +0100 Subject: [PATCH 12/14] Update doc from review --- .../docs/explainers/explainer-writing-noir.md | 40 ++++++++----------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index 4c47701773e..99c24757863 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -1,5 +1,5 @@ --- -title: Writing Noir +title: Writing Noir Well description: Understand new considerations when writing Noir keywords: [Noir, programming, rust] tags: [Optimization] @@ -18,14 +18,12 @@ When writing firmware for a battery-powered microcontroller, you think of cpu cy > Code is written to create applications that perform specific tasks within specific constraints And these constraints differ depending on where the compiled code is execute. -### The Ethereum Virtual Machine (EVM) -For the EVM, Solidity code is compiled into bytecode to be executed, and the gas cost of opcodes become as important as clock cycles. These gas costs were designed in to the protocol to reward machines that are executing bytecode for the EVM, so it is no surprise that a lot of the costs are roughly proportional to clock-cycles (a proxy for power consumption). Eg Addition: 3, Multiplication: 8. +### The Ethereum Virtual Machine (EVM) -But there is a twist, the cost of writing to disk is amplified, and is a few orders of magnitude larger. Namely: 20k for allocating and writing a new value, or 5k for writing an existing value. -So whilst writing efficient code for regular computers and the EVM is mostly the same, there are some key differences like this more immediate compensation of disk writes. +In scenarios where extremely low gas costs are required for an Ethereum application to be viable/competitive, Ethereum smart contract developers get into what is colloquially known as: "*gas golfing*". Finding the lowest execution cost of their compiled code (EVM bytecode) to achieve a specific task. -In scenarios where extremely low gas costs are required for an application to be viable/competitive, developers get into what is colloquially known as: "*gas golfing*". Finding the lowest execution cost to achieve a specific task. +The equivalent optimization task when writing zk circuits is affectionately referred to as "*gate golfing*", finding the lowest gate representation of the compiled Noir code. ### Coding for circuits - a paradigm shift @@ -71,7 +69,8 @@ Fortunately for Noir developers, when needing a particular function a Rust imple A few things to do when converting Rust code to Noir: - `println!` is not a macro, use `println` function (same for `assert_eq`) - No early `return` in function. Use constrain via assertion instead -- No reference `&` operator. Remove, will be value copy +- No passing by reference. Remove `&` operator to pass by value (copy) +- No boolean operators (`&&`, `||`). Use bitwise operators (`&`, `|`) with boolean values - No type `usize`. Use types `u8`, `u32`, `u64`, ... - `main` return must be public, `pub` - No `const`, use `global` @@ -95,7 +94,7 @@ A Noir program compiles to an Abstract Circuit Intermediate Representation which :::tip The command `nargo info` shows the programs circuit size, and is useful to compare the value of changes made. -Advanced: You can dig deeper and use the `--print-acir` param to take a closer look at individual gates too. +You can dig deeper and use the `--print-acir` param to take a closer look at individual ACIR opcodes, and the proving backend to see its gate count (eg for barretenberg, `bb gates -b ./target/program.json`). ::: ### Use the `Field` type @@ -114,7 +113,7 @@ Since circuits are made of arithmetic gates, the cost of arithmetic operations t Inversely, non-arithmetic operators are achieved with multiple gates, vs 1 clock cycle for procedural code. -| (cost\op) | arithmetic
(eg `*`, `+`) | bit-wise ops
(eg `<`, `\|`, `>>`) | +| (cost\op) | arithmetic
(`*`, `+`) | bit-wise ops
(eg `<`, `\|`, `>>`) | | - | - | - | | **cycles** | 10+ | 1 | | **gates** | 1 | 10+ | @@ -140,19 +139,12 @@ Using `assert_constant(i);` before an index, `i`, is used in an array will give ### Leverage unconstrained execution -Constrained verification can leverage unconstrained execution. +Constrained verification can leverage unconstrained execution, this is especially useful for operations that are represented by many gates. Compute result via unconstrained function, verify result. -Use ` if is_unconstrained() { /`, to conditionally execute code if being called in an unconstrained vs constrained way. - -### A circuit holds all logic - -In procedural execution, the following logic: `if (a>0 && b>0) {` , will not perform the second comparison if the first one is false. Whereas a circuit will hold both paths. -Implementing this type of short-circuiting is much less efficient for circuits since it is effectively adding additional comparisons which add more gates. +Eg division generates more gates than multiplication, so calculating the quotient in an unconstrained function then constraining the product for the quotient and divisor (+ any remainder) equals the dividend will be more efficient. -:::tip -Use bit-wise `&` or `|` to combine logical expressions efficiently. -::: +Use ` if is_unconstrained() { /`, to conditionally execute code if being called in an unconstrained vs constrained way. ## Advanced @@ -167,12 +159,14 @@ $ w_1*w_2*q_m + w_1*q_1 + w_2*q_2 + w_3*q_3 + w_4*q_4 + q_c = 0 $ Here we see there is one occurrence of witness 1 and 2 ($w_1$, $w_2$) being multiplied together, with addition to witnesses 1-4 ($w_1$ .. $w_4$) multiplied by 4 corresponding circuit constants ($q_1$ .. $q_4$) (plus a final circuit constant, $q_c$). -Use `nargo info --print-acir`, to inspect the constraints, and it may present opportunities to amend the order of operations and reduce the number of constraints. +Use `nargo info --print-acir`, to inspect the ACIR opcodes (and the proving system for gates), and it may present opportunities to amend the order of operations and reduce the number of constraints. + +#### Variable as witness vs expression -### Variable as witness vs expression +If you've come this far and really know what you're doing at the equation level, a temporary lever (that will become unnecessary/useless over time) is: `std::as_witness`. This informs the compiler to save a variable as a witness not an expression. -`std::as_witness` means variable is interpreted as a witness not an expression. -When used incorrectly will create **less** efficient circuits (higher gate count). +The compiler will mostly be correct and optimal, but this may help some near term edge cases that are yet to optimize. +Note: When used incorrectly it will create **less** efficient circuits (higher gate count). ## References - Guillaume's ["`Cryptdoku`" talk](https://www.youtube.com/watch?v=MrQyzuogxgg) (Jun'23) From c81ed51122e4547b1f992e04cb748ae8ebcf4d60 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Thu, 25 Jul 2024 14:53:11 +0100 Subject: [PATCH 13/14] Update doc from review --- docs/docs/explainers/explainer-writing-noir.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index 99c24757863..a0665af7b0c 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -140,7 +140,7 @@ Using `assert_constant(i);` before an index, `i`, is used in an array will give ### Leverage unconstrained execution Constrained verification can leverage unconstrained execution, this is especially useful for operations that are represented by many gates. -Compute result via unconstrained function, verify result. +Use an [unconstrained function](https://noir-lang.org/docs/noir/concepts/unconstrained) to perform gate-heavy calculations, then verify and constrain the result. Eg division generates more gates than multiplication, so calculating the quotient in an unconstrained function then constraining the product for the quotient and divisor (+ any remainder) equals the dividend will be more efficient. From 95e3496dce7787448ceb718af7fd1be237bd00ad Mon Sep 17 00:00:00 2001 From: James Zaki Date: Thu, 25 Jul 2024 17:27:31 +0100 Subject: [PATCH 14/14] Update doc from review --- docs/docs/explainers/explainer-writing-noir.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index a0665af7b0c..c8a42c379e6 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -1,12 +1,11 @@ --- -title: Writing Noir Well +title: Writing Performant Noir description: Understand new considerations when writing Noir keywords: [Noir, programming, rust] tags: [Optimization] sidebar_position: 0 --- -# Writing Noir for fun and profit This article intends to set you up with key concepts essential for writing more viable applications that use zero knowledge proofs, namely around efficient circuits. @@ -140,7 +139,7 @@ Using `assert_constant(i);` before an index, `i`, is used in an array will give ### Leverage unconstrained execution Constrained verification can leverage unconstrained execution, this is especially useful for operations that are represented by many gates. -Use an [unconstrained function](https://noir-lang.org/docs/noir/concepts/unconstrained) to perform gate-heavy calculations, then verify and constrain the result. +Use an [unconstrained function](../noir/concepts/unconstrained.md) to perform gate-heavy calculations, then verify and constrain the result. Eg division generates more gates than multiplication, so calculating the quotient in an unconstrained function then constraining the product for the quotient and divisor (+ any remainder) equals the dividend will be more efficient.