From 5300ec321fb99ddaad32e83f751aed28e175736f Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Thu, 9 Jan 2025 18:16:57 -0300 Subject: [PATCH 1/7] fix: require generic trait impls to be in scope to call them (#6913) Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- .../noirc_frontend/src/elaborator/types.rs | 80 +++++++++++-------- compiler/noirc_frontend/src/node_interner.rs | 45 +++-------- compiler/noirc_frontend/src/tests/traits.rs | 70 ++++++++++++++++ 3 files changed, 126 insertions(+), 69 deletions(-) diff --git a/compiler/noirc_frontend/src/elaborator/types.rs b/compiler/noirc_frontend/src/elaborator/types.rs index 9b3df4631b..72e46d36c2 100644 --- a/compiler/noirc_frontend/src/elaborator/types.rs +++ b/compiler/noirc_frontend/src/elaborator/types.rs @@ -1361,7 +1361,7 @@ impl<'context> Elaborator<'context> { span: Span, has_self_arg: bool, ) -> Option { - // First search in the struct methods + // First search in the type methods. If there is one, that's the one. if let Some(method_id) = self.interner.lookup_direct_method(object_type, method_name, has_self_arg) { @@ -1372,43 +1372,55 @@ impl<'context> Elaborator<'context> { let trait_methods = self.interner.lookup_trait_methods(object_type, method_name, has_self_arg); - if trait_methods.is_empty() { - // If we couldn't find any trait methods, search in - // impls for all types `T`, e.g. `impl Foo for T` - if let Some(func_id) = - self.interner.lookup_generic_method(object_type, method_name, has_self_arg) - { - return Some(HirMethodReference::FuncId(func_id)); - } + // If there's at least one matching trait method we need to see if only one is in scope. + if !trait_methods.is_empty() { + return self.return_trait_method_in_scope(&trait_methods, method_name, span); + } - if let Type::Struct(struct_type, _) = object_type { - let has_field_with_function_type = - struct_type.borrow().get_fields_as_written().into_iter().any(|field| { - field.name.0.contents == method_name && field.typ.is_function() - }); - if has_field_with_function_type { - self.push_err(TypeCheckError::CannotInvokeStructFieldFunctionType { - method_name: method_name.to_string(), - object_type: object_type.clone(), - span, - }); - } else { - self.push_err(TypeCheckError::UnresolvedMethodCall { - method_name: method_name.to_string(), - object_type: object_type.clone(), - span, - }); - } - return None; + // If we couldn't find any trait methods, search in + // impls for all types `T`, e.g. `impl Foo for T` + let generic_methods = + self.interner.lookup_generic_methods(object_type, method_name, has_self_arg); + if !generic_methods.is_empty() { + return self.return_trait_method_in_scope(&generic_methods, method_name, span); + } + + if let Type::Struct(struct_type, _) = object_type { + let has_field_with_function_type = struct_type + .borrow() + .get_fields_as_written() + .into_iter() + .any(|field| field.name.0.contents == method_name && field.typ.is_function()); + if has_field_with_function_type { + self.push_err(TypeCheckError::CannotInvokeStructFieldFunctionType { + method_name: method_name.to_string(), + object_type: object_type.clone(), + span, + }); } else { - // It could be that this type is a composite type that is bound to a trait, - // for example `x: (T, U) ... where (T, U): SomeTrait` - // (so this case is a generalization of the NamedGeneric case) - return self.lookup_method_in_trait_constraints(object_type, method_name, span); + self.push_err(TypeCheckError::UnresolvedMethodCall { + method_name: method_name.to_string(), + object_type: object_type.clone(), + span, + }); } + None + } else { + // It could be that this type is a composite type that is bound to a trait, + // for example `x: (T, U) ... where (T, U): SomeTrait` + // (so this case is a generalization of the NamedGeneric case) + self.lookup_method_in_trait_constraints(object_type, method_name, span) } + } - // We found some trait methods... but is only one of them currently in scope? + /// Given a list of functions and the trait they belong to, returns the one function + /// that is in scope. + fn return_trait_method_in_scope( + &mut self, + trait_methods: &[(FuncId, TraitId)], + method_name: &str, + span: Span, + ) -> Option { let module_id = self.module_id(); let module_data = self.get_module(module_id); @@ -1490,7 +1502,7 @@ impl<'context> Elaborator<'context> { fn trait_hir_method_reference( &self, trait_id: TraitId, - trait_methods: Vec<(FuncId, TraitId)>, + trait_methods: &[(FuncId, TraitId)], method_name: &str, span: Span, ) -> HirMethodReference { diff --git a/compiler/noirc_frontend/src/node_interner.rs b/compiler/noirc_frontend/src/node_interner.rs index c1a405535f..a2b1d7fc8f 100644 --- a/compiler/noirc_frontend/src/node_interner.rs +++ b/compiler/noirc_frontend/src/node_interner.rs @@ -1746,7 +1746,7 @@ impl NodeInterner { Ok(()) } - /// Looks up a method that's directly defined in the given struct. + /// Looks up a method that's directly defined in the given type. pub fn lookup_direct_method( &self, typ: &Type, @@ -1761,7 +1761,7 @@ impl NodeInterner { .and_then(|methods| methods.find_direct_method(typ, has_self_arg, self)) } - /// Looks up a methods that apply to the given struct but are defined in traits. + /// Looks up a methods that apply to the given type but are defined in traits. pub fn lookup_trait_methods( &self, typ: &Type, @@ -1780,43 +1780,18 @@ impl NodeInterner { } } - /// Select the 1 matching method with an object type matching `typ` - fn find_matching_method( - &self, - typ: &Type, - methods: Option<&Methods>, - method_name: &str, - has_self_arg: bool, - ) -> Option { - if let Some(method) = methods.and_then(|m| m.find_matching_method(typ, has_self_arg, self)) - { - Some(method) - } else { - self.lookup_generic_method(typ, method_name, has_self_arg) - } - } - - /// Looks up a method at impls for all types `T`, e.g. `impl Foo for T` - pub fn lookup_generic_method( + /// Looks up methods at impls for all types `T`, e.g. `impl Foo for T` + pub fn lookup_generic_methods( &self, typ: &Type, method_name: &str, has_self_arg: bool, - ) -> Option { - let global_methods = self.methods.get(&TypeMethodKey::Generic)?.get(method_name)?; - global_methods.find_matching_method(typ, has_self_arg, self) - } - - /// Looks up a given method name on the given primitive type. - pub fn lookup_primitive_method( - &self, - typ: &Type, - method_name: &str, - has_self_arg: bool, - ) -> Option { - let key = get_type_method_key(typ)?; - let methods = self.methods.get(&key)?.get(method_name)?; - self.find_matching_method(typ, Some(methods), method_name, has_self_arg) + ) -> Vec<(FuncId, TraitId)> { + self.methods + .get(&TypeMethodKey::Generic) + .and_then(|h| h.get(method_name)) + .map(|methods| methods.find_trait_methods(typ, has_self_arg, self)) + .unwrap_or_default() } /// Returns what the next trait impl id is expected to be. diff --git a/compiler/noirc_frontend/src/tests/traits.rs b/compiler/noirc_frontend/src/tests/traits.rs index f573045131..3a55fc2b67 100644 --- a/compiler/noirc_frontend/src/tests/traits.rs +++ b/compiler/noirc_frontend/src/tests/traits.rs @@ -879,6 +879,43 @@ fn errors_if_multiple_trait_methods_are_in_scope_for_function_call() { assert_eq!(traits, vec!["private_mod::Foo", "private_mod::Foo2"]); } +#[test] +fn warns_if_trait_is_not_in_scope_for_method_call_and_there_is_only_one_trait_method() { + let src = r#" + fn main() { + let bar = Bar { x: 42 }; + let _ = bar.foo(); + } + + pub struct Bar { + x: i32, + } + + mod private_mod { + pub trait Foo { + fn foo(self) -> i32; + } + + impl Foo for super::Bar { + fn foo(self) -> i32 { + self.x + } + } + } + "#; + let errors = get_program_errors(src); + assert_eq!(errors.len(), 1); + + let CompilationError::ResolverError(ResolverError::PathResolutionError( + PathResolutionError::TraitMethodNotInScope { ident, trait_name }, + )) = &errors[0].0 + else { + panic!("Expected a 'trait method not in scope' error"); + }; + assert_eq!(ident.to_string(), "foo"); + assert_eq!(trait_name, "private_mod::Foo"); +} + #[test] fn calls_trait_method_if_it_is_in_scope() { let src = r#" @@ -1166,3 +1203,36 @@ fn warns_if_trait_is_not_in_scope_for_primitive_method_call_and_there_is_only_on assert_eq!(ident.to_string(), "foo"); assert_eq!(trait_name, "private_mod::Foo"); } + +#[test] +fn warns_if_trait_is_not_in_scope_for_generic_function_call_and_there_is_only_one_trait_method() { + let src = r#" + fn main() { + let x: i32 = 1; + let _ = x.foo(); + } + + mod private_mod { + pub trait Foo { + fn foo(self) -> i32; + } + + impl Foo for T { + fn foo(self) -> i32 { + 42 + } + } + } + "#; + let errors = get_program_errors(src); + assert_eq!(errors.len(), 1); + + let CompilationError::ResolverError(ResolverError::PathResolutionError( + PathResolutionError::TraitMethodNotInScope { ident, trait_name }, + )) = &errors[0].0 + else { + panic!("Expected a 'trait method not in scope' error"); + }; + assert_eq!(ident.to_string(), "foo"); + assert_eq!(trait_name, "private_mod::Foo"); +} From 2903052b5a7e74b0a14464d26d8b687bff4454eb Mon Sep 17 00:00:00 2001 From: Lisa <106527861+sthwnd@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:26:16 +0200 Subject: [PATCH 2/7] chore: clarity fix in docs (#7016) Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- docs/docs/getting_started/quick_start.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/getting_started/quick_start.md b/docs/docs/getting_started/quick_start.md index 7deeae12fd..c980b5e7ff 100644 --- a/docs/docs/getting_started/quick_start.md +++ b/docs/docs/getting_started/quick_start.md @@ -98,7 +98,7 @@ bb prove -b ./target/hello_world.json -w ./target/hello_world.gz -o ./target/pro :::tip -Naming can be confusing, specially as you pass them to the `bb` commands. If unsure, it won't hurt to delete the target folder and start anew to make sure you're using the most recent versions of the compiled circuit and witness. +Naming can be confusing, specially as you pass them to the `bb` commands. If unsure, it won't hurt to delete the target folder and start fresh to make sure you're using the most recent versions of the compiled circuit and witness. ::: From 24433fec92c4e0cab6a1f4a49c633c3e33219107 Mon Sep 17 00:00:00 2001 From: Tom French <15848336+TomAFrench@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:58:46 +0000 Subject: [PATCH 3/7] chore: mark `aztec-nr` as expected to compile (#7015) --- ...nr.failures.jsonl.does_not_compile => aztec-nr.failures.jsonl} | 0 ...lures.jsonl.does_not_compile => noir-contracts.failures.jsonl} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/{aztec-nr.failures.jsonl.does_not_compile => aztec-nr.failures.jsonl} (100%) rename .github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/{noir-contracts.failures.jsonl.does_not_compile => noir-contracts.failures.jsonl} (100%) diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/aztec-nr.failures.jsonl.does_not_compile b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/aztec-nr.failures.jsonl similarity index 100% rename from .github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/aztec-nr.failures.jsonl.does_not_compile rename to .github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/aztec-nr.failures.jsonl diff --git a/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-contracts.failures.jsonl.does_not_compile b/.github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-contracts.failures.jsonl similarity index 100% rename from .github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-contracts.failures.jsonl.does_not_compile rename to .github/critical_libraries_status/AztecProtocol/aztec-packages/noir-projects/noir-contracts.failures.jsonl From 268229e8e1497472dc514baeb792985677963735 Mon Sep 17 00:00:00 2001 From: Ary Borenszweig Date: Fri, 10 Jan 2025 10:33:17 -0300 Subject: [PATCH 4/7] fix: let static_assert fail with the provided message (#7005) Co-authored-by: jfecher Co-authored-by: Tom French <15848336+TomAFrench@users.noreply.github.com> --- compiler/noirc_evaluator/src/errors.rs | 6 +++--- compiler/noirc_evaluator/src/ssa/ir/dfg.rs | 18 ++++++++++++++++ .../noirc_evaluator/src/ssa/ir/printer.rs | 21 +++++-------------- .../src/ssa/opt/assert_constant.rs | 6 +++++- noir_stdlib/src/lib.nr | 7 +++++++ 5 files changed, 38 insertions(+), 20 deletions(-) diff --git a/compiler/noirc_evaluator/src/errors.rs b/compiler/noirc_evaluator/src/errors.rs index 1e484f8af5..94c0e0554a 100644 --- a/compiler/noirc_evaluator/src/errors.rs +++ b/compiler/noirc_evaluator/src/errors.rs @@ -49,8 +49,8 @@ pub enum RuntimeError { StaticAssertDynamicMessage { call_stack: CallStack }, #[error("Argument is dynamic")] StaticAssertDynamicPredicate { call_stack: CallStack }, - #[error("Argument is false")] - StaticAssertFailed { call_stack: CallStack }, + #[error("{message}")] + StaticAssertFailed { message: String, call_stack: CallStack }, #[error("Nested slices, i.e. slices within an array or slice, are not supported")] NestedSlice { call_stack: CallStack }, #[error("Big Integer modulus do no match")] @@ -165,7 +165,7 @@ impl RuntimeError { | RuntimeError::AssertConstantFailed { call_stack } | RuntimeError::StaticAssertDynamicMessage { call_stack } | RuntimeError::StaticAssertDynamicPredicate { call_stack } - | RuntimeError::StaticAssertFailed { call_stack } + | RuntimeError::StaticAssertFailed { call_stack, .. } | RuntimeError::IntegerOutOfBounds { call_stack, .. } | RuntimeError::UnsupportedIntegerSize { call_stack, .. } | RuntimeError::InvalidBlackBoxInputBitSize { call_stack, .. } diff --git a/compiler/noirc_evaluator/src/ssa/ir/dfg.rs b/compiler/noirc_evaluator/src/ssa/ir/dfg.rs index 8425e4d5e5..57e833af7e 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/dfg.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/dfg.rs @@ -525,6 +525,24 @@ impl DataFlowGraph { } } + /// If this value points to an array of constant bytes, returns a string + /// consisting of those bytes if they form a valid UTF-8 string. + pub(crate) fn get_string(&self, value: ValueId) -> Option { + let (value_ids, _typ) = self.get_array_constant(value)?; + + let mut bytes = Vec::new(); + for value_id in value_ids { + let field_value = self.get_numeric_constant(value_id)?; + let u64_value = field_value.try_to_u64()?; + if u64_value > 255 { + return None; + }; + let byte = u64_value as u8; + bytes.push(byte); + } + String::from_utf8(bytes).ok() + } + /// A constant index less than the array length is safe pub(crate) fn is_safe_index(&self, index: ValueId, array: ValueId) -> bool { #[allow(clippy::match_like_matches_macro)] diff --git a/compiler/noirc_evaluator/src/ssa/ir/printer.rs b/compiler/noirc_evaluator/src/ssa/ir/printer.rs index 9bf26b8414..d30fce170d 100644 --- a/compiler/noirc_evaluator/src/ssa/ir/printer.rs +++ b/compiler/noirc_evaluator/src/ssa/ir/printer.rs @@ -274,22 +274,11 @@ pub(crate) fn try_to_extract_string_from_error_payload( values: &[ValueId], dfg: &DataFlowGraph, ) -> Option { - (is_string_type && (values.len() == 1)) - .then_some(()) - .and_then(|()| { - let (values, _) = &dfg.get_array_constant(values[0])?; - let values = values.iter().map(|value_id| dfg.get_numeric_constant(*value_id)); - values.collect::>>() - }) - .map(|fields| { - fields - .iter() - .map(|field| { - let as_u8 = field.try_to_u64().unwrap_or_default() as u8; - as_u8 as char - }) - .collect() - }) + if is_string_type && values.len() == 1 { + dfg.get_string(values[0]) + } else { + None + } } fn display_constrain_error( diff --git a/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs b/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs index 6936c7ad54..192c0f5934 100644 --- a/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs +++ b/compiler/noirc_evaluator/src/ssa/opt/assert_constant.rs @@ -122,7 +122,11 @@ fn evaluate_static_assert( } else { let call_stack = function.dfg.get_instruction_call_stack(instruction); if function.dfg.is_constant(arguments[0]) { - Err(RuntimeError::StaticAssertFailed { call_stack }) + let message = function + .dfg + .get_string(arguments[1]) + .expect("Expected second argument to be a string"); + Err(RuntimeError::StaticAssertFailed { message, call_stack }) } else { Err(RuntimeError::StaticAssertDynamicPredicate { call_stack }) } diff --git a/noir_stdlib/src/lib.nr b/noir_stdlib/src/lib.nr index 2e743822ff..fb073516d2 100644 --- a/noir_stdlib/src/lib.nr +++ b/noir_stdlib/src/lib.nr @@ -120,3 +120,10 @@ where #[builtin(as_witness)] pub fn as_witness(x: Field) {} + +mod tests { + #[test(should_fail_with = "custom message")] + fn test_static_assert_custom_message() { + super::static_assert(1 == 2, "custom message"); + } +} From 8c92b70d1605325fb43017eab401744cb57587e9 Mon Sep 17 00:00:00 2001 From: James Zaki Date: Fri, 10 Jan 2025 15:14:09 +0000 Subject: [PATCH 5/7] chore: Add more Field use info (#7019) --- .../docs/explainers/explainer-writing-noir.md | 45 +++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/docs/docs/explainers/explainer-writing-noir.md b/docs/docs/explainers/explainer-writing-noir.md index 202bf76a82..b4265a14db 100644 --- a/docs/docs/explainers/explainer-writing-noir.md +++ b/docs/docs/explainers/explainer-writing-noir.md @@ -26,9 +26,9 @@ The equivalent optimization task when writing zk circuits is affectionately refe ### Coding for circuits - a paradigm shift -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. +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 proof size and proving time, so from a product point of view 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 have some 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... For example, drawing a circle at (0, 0) of radius `r`: - For a single CPU thread, @@ -57,7 +57,7 @@ For those coming from a primarily web app background, this article will explain ## 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. +Programs written in anything from pseudo code to C, can be translated into Noir. A Rust program written for execution can be readily ported to Noir thanks to the similarities in syntax. :::note Many valuable functions and algorithms have been written in more established languages (C/C++), and converted to modern ones (like Rust). @@ -93,23 +93,42 @@ 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. -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`). +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, the `bb` binary has a `gates` option). ::: -### Use the `Field` type +### Numerical types -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. -Some things to be mindful of when using a Field type for a regular integer value: -- A variable of type `Field` can be cast `as` an integer type (eg `u8`, `u64`) - - Note: this retains only the bits of the integer type. Eg a Field value of 260 as a `u8` becomes 4 -- For Field types arithmetic operations meaningfully overflow/underflow, yet for integer types they are checked according to their size -- Comparisons and bitwise operations do not exist for `Field`s, cast to an appropriately sized integer type when you need to +As mentioned earlier Noir has many familiar integer types (eg `i8`, `u64`). Ideally after bringing a program into Noir, proving/verifying of its execution just works where needed: client/server side, on an evm, or on the Aztec network. + +A program optimized for execution may leverage the binary representations of integers, reducing the number of clock cycles, and thus time of execution. +The cryptography in a proving backend makes use of a `Field` type, and leveraging this lower level type correctly can reduce gate count, and thus proof size and proving time. + +In some instances simply replacing the integer type with a `Field` could save on some range checks (and hence gates). +Note: when casting a `Field` to an integer type, the value is converted based on the integer binary representation. Eg a Field variable with a value of 260 `as u8` becomes 4 + +### `Field`s for efficiency + +`Field` types have their own underlying representation that is efficient for cryptography, which is different to binary representations efficient for CPUs. So, mathematically speaking, things like bitwise operations do not directly translate to fields. That said, the same outcome can be achieved if wanting to use the Field type as a number with lower overhead. + +For instance shift (`<<`) and or (`|`) work seamlessly with integer types (bit-packing `u8`'s into a `u16`): +``` + high as u16 << 8 | low as u16 +``` + +More efficiently with `Field` types, the equivalent is: +``` + low.assert_max_bit_size::<8>(); // ensure Field values could be represented as 8 bit numbers + high.assert_max_bit_size::<8>(); + (high * 2.pow_32(8) + low) +``` +(Note, the power of two can instead be a constant (256) or global evaluated at compile time) + +The first snippet is good for compatibility when using existing code, converting to the latter can help optimize frequently used functions. :::tip -Where possible, use `Field` type for values. Using smaller value types, and bit-packing strategies, will result in MORE gates +Where possible, use the `Field` type for values. Writing code with smaller value types and bit-packing strategies will result in MORE gates ::: - ### Use Arithmetic over non-arithmetic operations 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. From fc5848c7e24a3708ed8c3c378d081895997bfb54 Mon Sep 17 00:00:00 2001 From: Akosh Farkash Date: Fri, 10 Jan 2025 15:58:48 +0000 Subject: [PATCH 6/7] feat(cli): Add CLI option to filter by contract function name (#7018) --- compiler/noirc_driver/src/lib.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/compiler/noirc_driver/src/lib.rs b/compiler/noirc_driver/src/lib.rs index 87da9cd658..a7e7e2d4e2 100644 --- a/compiler/noirc_driver/src/lib.rs +++ b/compiler/noirc_driver/src/lib.rs @@ -73,7 +73,11 @@ pub struct CompileOptions { /// Only show SSA passes whose name contains the provided string. /// This setting takes precedence over `show_ssa` if it's not empty. #[arg(long, hide = true)] - pub show_ssa_pass_name: Option, + pub show_ssa_pass: Option, + + /// Only show the SSA and ACIR for the contract function with a given name. + #[arg(long, hide = true)] + pub show_contract_fn: Option, /// Emit the unoptimized SSA IR to file. /// The IR will be dumped into the workspace target directory, @@ -442,6 +446,11 @@ pub fn compile_contract( if options.print_acir { for contract_function in &compiled_contract.functions { + if let Some(ref name) = options.show_contract_fn { + if name != &contract_function.name { + continue; + } + } println!( "Compiled ACIR for {}::{} (unoptimized):", compiled_contract.name, contract_function.name @@ -486,7 +495,15 @@ fn compile_contract_inner( continue; } - let function = match compile_no_check(context, options, function_id, None, true) { + let mut options = options.clone(); + + if let Some(ref name_filter) = options.show_contract_fn { + let show = name == *name_filter; + options.show_ssa &= show; + options.show_ssa_pass = options.show_ssa_pass.filter(|_| show); + }; + + let function = match compile_no_check(context, &options, function_id, None, true) { Ok(function) => function, Err(new_error) => { errors.push(FileDiagnostic::from(new_error)); @@ -642,7 +659,7 @@ pub fn compile_no_check( let return_visibility = program.return_visibility; let ssa_evaluator_options = noirc_evaluator::ssa::SsaEvaluatorOptions { - ssa_logging: match &options.show_ssa_pass_name { + ssa_logging: match &options.show_ssa_pass { Some(string) => SsaLogging::Contains(string.clone()), None => { if options.show_ssa { From 7b7d7c624b035d4e90e9ae26a07f5cd44d8fa46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro=20Sousa?= Date: Fri, 10 Jan 2025 17:03:27 +0100 Subject: [PATCH 7/7] chore(docs): backport 1.0.0-beta.0 doc fixes (#7014) --- docs/docs/tutorials/noirjs_app.md | 6 +- ...rifier.md => how-to-solidity-verifier.mdx} | 141 ++++--- .../tutorials/noirjs_app.md | 380 ++++++++---------- 3 files changed, 254 insertions(+), 273 deletions(-) rename docs/versioned_docs/version-v1.0.0-beta.0/how_to/{how-to-solidity-verifier.md => how-to-solidity-verifier.mdx} (64%) diff --git a/docs/docs/tutorials/noirjs_app.md b/docs/docs/tutorials/noirjs_app.md index 8967ee005c..cc61fff840 100644 --- a/docs/docs/tutorials/noirjs_app.md +++ b/docs/docs/tutorials/noirjs_app.md @@ -25,7 +25,7 @@ Let's go barebones. Doing the bare minimum is not only simple, but also allows y Barebones means we can immediately start with the dependencies even on an empty folder 😈: ```bash -bun i @noir-lang/noir_wasm@1.0.0-beta.0 @noir-lang/noir_js@1.0.0-beta.0 @aztec/bb.js@0.63.1 +bun i @noir-lang/noir_wasm@1.0.0-beta.1 @noir-lang/noir_js@1.0.0-beta.1 @aztec/bb.js@0.63.1 ``` Wait, what are these dependencies? @@ -36,7 +36,7 @@ Wait, what are these dependencies? :::info -In this guide, we will install versions pinned to 1.0.0-beta.0. These work with Barretenberg version 0.63.1, so we are using that one version too. Feel free to try with older or later versions, though! +In this guide, we will install versions pinned to 1.0.0-beta.1. These work with Barretenberg version 0.63.1, so we are using that one version too. Feel free to try with older or later versions, though! ::: @@ -50,7 +50,7 @@ It's not just you. We also enjoy syntax highlighting. [Check out the Language Se ::: -All you need is a `main.nr` and a `Nargo.toml` file. You can follow the [noirup](../getting_started/noir_installation.md) installation and just run `noirup -v 1.0.0-beta.0`, or just create them by hand: +All you need is a `main.nr` and a `Nargo.toml` file. You can follow the [noirup](../getting_started/noir_installation.md) installation and just run `noirup -v 1.0.0-beta.1`, or just create them by hand: ```bash mkdir -p circuit/src diff --git a/docs/versioned_docs/version-v1.0.0-beta.0/how_to/how-to-solidity-verifier.md b/docs/versioned_docs/version-v1.0.0-beta.0/how_to/how-to-solidity-verifier.mdx similarity index 64% rename from docs/versioned_docs/version-v1.0.0-beta.0/how_to/how-to-solidity-verifier.md rename to docs/versioned_docs/version-v1.0.0-beta.0/how_to/how-to-solidity-verifier.mdx index 2cc0f8e57c..da36b60920 100644 --- a/docs/versioned_docs/version-v1.0.0-beta.0/how_to/how-to-solidity-verifier.md +++ b/docs/versioned_docs/version-v1.0.0-beta.0/how_to/how-to-solidity-verifier.mdx @@ -20,24 +20,37 @@ sidebar_position: 0 pagination_next: tutorials/noirjs_app --- -Noir has the ability to generate a verifier contract in Solidity, which can be deployed in many EVM-compatible blockchains such as Ethereum. +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Noir is universal. The witness and the compiled program can be fed into a proving backend such as Aztec's [Barretenberg](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg), which can then generate a verifier contract for deployment on blockchains. This allows for a powerful feature set, as one can make use of the conciseness and the privacy provided by Noir in an immutable ledger. Applications can range from simple P2P guessing games, to complex private DeFi interactions. -This guide shows you how to generate a Solidity Verifier and deploy it on the [Remix IDE](https://remix.ethereum.org/). It is assumed that: +Although not strictly in the domain of Noir itself, this guide shows how to generate a Solidity Verifier with Barretenberg and deploy it on the [Remix IDE](https://remix.ethereum.org/). It is assumed that: +- You will be using Barretenberg as your proving backend +- You will be using an EVM blockchain to verify your proof - You are comfortable with the Solidity programming language and understand how contracts are deployed on the Ethereum network - You have Noir installed and you have a Noir program. If you don't, [get started](../getting_started/quick_start.md) with Nargo and the example Hello Noir circuit - You are comfortable navigating RemixIDE. If you aren't or you need a refresher, you can find some video tutorials [here](https://www.youtube.com/channel/UCjTUPyFEr2xDGN6Cg8nKDaA) that could help you. ## Rundown -Generating a Solidity Verifier contract is actually a one-command process. However, compiling it and deploying it can have some caveats. Here's the rundown of this guide: +Generating a Solidity Verifier with Barretenberg contract is actually a one-command process. However, compiling it and deploying it can have some caveats. Here's the rundown of this guide: 1. How to generate a solidity smart contract 2. How to compile the smart contract in the RemixIDE 3. How to deploy it to a testnet +:::info[Which proving system to use?] + +Barretenberg currently provides two provers: `UltraPlonk` and `UltraHonk`. In a nutshell, `UltraHonk` is faster and uses less RAM, but its verifier contract is much more expensive. `UltraPlonk` is optimized for on-chain verification, but proving is more expensive. + +In any case, we provide instructions for both. Choose your poison ☠️ + +::: + ## Step 1 - Generate a contract This is by far the most straightforward step. Just run: @@ -46,25 +59,31 @@ This is by far the most straightforward step. Just run: nargo compile ``` -This will compile your source code into a Noir build artifact to be stored in the `./target` directory, you can then generate the smart contract using the commands: +This will compile your source code into a Noir build artifact to be stored in the `./target` directory. From here on, it's Barretenberg's work. You can generate the smart contract using the commands: + + + + +```sh +bb write_vk_ultra_keccak_honk -b ./target/.json +bb contract_ultra_honk +``` + + + ```sh -# Here we pass the path to the newly generated Noir artifact. bb write_vk -b ./target/.json bb contract ``` -replacing `` with the name of your Noir project. A new `contract` folder would then be generated in your project directory, containing the Solidity -file `contract.sol`. It can be deployed to any EVM blockchain acting as a verifier smart contract. + + -You can find more information about `bb` and the default Noir proving backend on [this page](../getting_started/quick_start.md#proving-backend). +replacing `` with the name of your Noir project. A `Verifier.sol` contract is now in the target folder and can be deployed to any EVM blockchain acting as a verifier smart contract. -:::info - -It is possible to generate verifier contracts of Noir programs for other smart contract platforms as long as the proving backend supplies an implementation. +You can find more information about `bb` and the default Noir proving backend on [this page](../getting_started/quick_start.md#proving-backend). -Barretenberg, the default proving backend for Nargo, supports generation of verifier contracts, for the time being these are only in Solidity. -::: ## Step 2 - Compiling @@ -85,17 +104,12 @@ To compile our the verifier, we can navigate to the compilation tab: ![Compilation Tab](@site/static/img/how-tos/solidity_verifier_2.png) -Remix should automatically match a suitable compiler version. However, hitting the "Compile" button will most likely generate a "Stack too deep" error: +Remix should automatically match a suitable compiler version. However, hitting the "Compile" button will most likely tell you the contract is too big to deploy on mainnet, or complain about a stack too deep: -![Stack too deep](@site/static/img/how-tos/solidity_verifier_3.png) +![Contract code too big](@site/static/img/how-tos/solidity_verifier_6.png) +![Stack too deep](@site/static/img/how-tos/solidity_verifier_8.png) -This is due to the verify function needing to put many variables on the stack, but enabling the optimizer resolves the issue. To do this, let's open the "Advanced Configurations" tab and enable optimization. The default 200 runs will suffice. - -:::info - -This time we will see a warning about an unused function parameter. This is expected, as the `verify` function doesn't use the `_proof` parameter inside a solidity block, it is loaded from calldata and used in assembly. - -::: +To avoid this, you can just use some optimization. Open the "Advanced Configurations" tab and enable optimization. The default 200 runs will suffice. ![Compilation success](@site/static/img/how-tos/solidity_verifier_4.png) @@ -103,54 +117,81 @@ This time we will see a warning about an unused function parameter. This is expe At this point we should have a compiled contract ready to deploy. If we navigate to the deploy section in Remix, we will see many different environments we can deploy to. The steps to deploy on each environment would be out-of-scope for this guide, so we will just use the default Remix VM. -Looking closely, we will notice that our "Solidity Verifier" is actually three contracts working together: - -- An `UltraVerificationKey` library which simply stores the verification key for our circuit. -- An abstract contract `BaseUltraVerifier` containing most of the verifying logic. -- A main `UltraVerifier` contract that inherits from the Base and uses the Key contract. - -Remix will take care of the dependencies for us so we can simply deploy the UltraVerifier contract by selecting it and hitting "deploy": +Looking closely, we will notice that our "Solidity Verifier" is composed on multiple contracts working together. Remix will take care of the dependencies for us so we can simply deploy the Verifier contract by selecting it and hitting "deploy": -![Deploying UltraVerifier](@site/static/img/how-tos/solidity_verifier_5.png) + + -A contract will show up in the "Deployed Contracts" section, where we can retrieve the Verification Key Hash. This is particularly useful for double-checking that the deployer contract is the correct one. +![Deploying HonkVerifier](@site/static/img/how-tos/solidity_verifier_7.png) -:::note + + -Why "UltraVerifier"? +![Deploying PlonkVerifier](@site/static/img/how-tos/solidity_verifier_9.png) -To be precise, the Noir compiler (`nargo`) doesn't generate the verifier contract directly. It compiles the Noir code into an intermediate language (ACIR), which is then executed by the backend. So it is the backend that returns the verifier smart contract, not Noir. + + -In this case, the Barretenberg Backend uses the UltraPlonk proving system, hence the "UltraVerifier" name. - -::: +A contract will show up in the "Deployed Contracts" section. ## Step 4 - Verifying -To verify a proof using the Solidity verifier contract, we call the `verify` function in this extended contract: +To verify a proof using the Solidity verifier contract, we call the `verify` function: ```solidity function verify(bytes calldata _proof, bytes32[] calldata _publicInputs) external view returns (bool) ``` -When using the default example in the [Hello Noir](../getting_started/quick_start.md) guide, the easiest way to confirm that the verifier contract is doing its job is by calling the `verify` function via remix with the required parameters. Note that the public inputs must be passed in separately to the rest of the proof so we must split the proof as returned from `bb`. +First generate a proof with `bb`. We need a `Prover.toml` file for our inputs. Run: -First generate a proof with `bb` at the location `./proof` using the steps in [get started](../getting_started/quick_start.md), this proof is in a binary format but we want to convert it into a hex string to pass into Remix, this can be done with the +```bash +nargo check +``` + +This will generate a `Prover.toml` you can fill with the values you want to prove. We can now execute the circuit with `nargo` and then use the proving backend to prove: + + + + +```bash +nargo execute +bb prove_ultra_keccak_honk -b ./target/.json -w ./target/ -o ./target/proof +``` + +:::tip[Public inputs] +Barretenberg attaches the public inputs to the proof, which in this case it's not very useful. If you're up for some JS, `bb.js` has [a method for it](https://github.com/AztecProtocol/aztec-packages/blob/master/barretenberg/ts/src/proof/index.ts), but in the CLI you can use this ugly snippet: + +```bash +cat ./target/proof | od -An -v -t x1 | tr -d $' \n' | sed 's/^.\{8\}//' | (read hex; echo "${hex:0:192}${hex:256}") +``` + +Beautiful. This assumes a circuit with one public input (32 bytes, for Barretenberg). For more inputs, you can just increment `hex:256` with 32 more bytes for each public input. + +::: + + + ```bash -# This value must be changed to match the number of public inputs (including return values!) in your program. -NUM_PUBLIC_INPUTS=1 -PUBLIC_INPUT_BYTES=32*NUM_PUBLIC_INPUTS -HEX_PUBLIC_INPUTS=$(head -c $PUBLIC_INPUT_BYTES ./proof | od -An -v -t x1 | tr -d $' \n') -HEX_PROOF=$(tail -c +$(($PUBLIC_INPUT_BYTES + 1)) ./proof | od -An -v -t x1 | tr -d $' \n') +nargo execute +bb prove -b ./target/.json -w ./target/ -o ./target/proof +``` + -echo "Public inputs:" -echo $HEX_PUBLIC_INPUTS +:::tip[Public inputs] +Barretenberg attaches the public inputs to the proof, which in this case it's not very useful. If you're up for some JS, `bb.js` has [a method for it](https://github.com/AztecProtocol/aztec-packages/blob/master/barretenberg/ts/src/proof/index.ts), but in the CLI you can use this ugly snippet: -echo "Proof:" -echo "0x$HEX_PROOF" +```bash +tail -c +33 ./target/proof | od -An -v -t x1 | tr -d $' \n' ``` +Beautiful. This assumes a circuit with one public input (32 bytes, for Barretenberg). For more inputs, you can just add 32 more bytes for each public input to the `tail` command. + +::: + + + + Remix expects that the public inputs will be split into an array of `bytes32` values so `HEX_PUBLIC_INPUTS` needs to be split up into 32 byte chunks which are prefixed with `0x` accordingly. A programmatic example of how the `verify` function is called can be seen in the example zk voting application [here](https://github.com/noir-lang/noir-examples/blob/33e598c257e2402ea3a6b68dd4c5ad492bce1b0a/foundry-voting/src/zkVote.sol#L35): @@ -188,7 +229,7 @@ the `verify` function will expect the public inputs array (second function param Passing only two inputs will result in an error such as `PUBLIC_INPUT_COUNT_INVALID(3, 2)`. -In this case, the inputs parameter to `verify` would be an array ordered as `[pubkey_x, pubkey_y, return`. +In this case, the inputs parameter to `verify` would be an array ordered as `[pubkey_x, pubkey_y, return]`. ::: diff --git a/docs/versioned_docs/version-v1.0.0-beta.0/tutorials/noirjs_app.md b/docs/versioned_docs/version-v1.0.0-beta.0/tutorials/noirjs_app.md index 6e69ea0bbe..8967ee005c 100644 --- a/docs/versioned_docs/version-v1.0.0-beta.0/tutorials/noirjs_app.md +++ b/docs/versioned_docs/version-v1.0.0-beta.0/tutorials/noirjs_app.md @@ -1,186 +1,91 @@ --- -title: Building a web app with NoirJS +title: Building a web app with Noir and Barretenberg description: Learn how to setup a new app that uses Noir to generate and verify zero-knowledge SNARK proofs in a typescript or javascript environment. keywords: [how to, guide, javascript, typescript, noir, barretenberg, zero-knowledge, proofs, app] sidebar_position: 0 pagination_next: noir/concepts/data_types/index --- -NoirJS is a set of packages meant to work both in a browser and a server environment. In this tutorial, we will build a simple web app using them. From here, you should get an idea on how to proceed with your own Noir projects! +NoirJS is a Typescript package meant to work both in a browser and a server environment. -You can find the complete app code for this guide [here](https://github.com/noir-lang/tiny-noirjs-app). - -## Setup - -:::note - -Feel free to use whatever versions, just keep in mind that Nargo and the NoirJS packages are meant to be in sync. For example, Nargo 0.31.x matches `noir_js@0.31.x`, etc. - -In this guide, we will be pinned to 0.31.0. - -::: - -Before we start, we want to make sure we have Node, Nargo and the Barretenberg proving system (`bb`) installed. - -We start by opening a terminal and executing `node --version`. If we don't get an output like `v20.10.0`, that means node is not installed. Let's do that by following the handy [nvm guide](https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script). - -As for `Nargo`, we can follow the [Nargo guide](../getting_started/quick_start.md) to install it. If you're lazy, just paste this on a terminal and run `noirup`: - -```sh -curl -L https://raw.githubusercontent.com/noir-lang/noirup/main/install | bash -``` +In this tutorial, we will combine NoirJS with Aztec's Barretenberg backend to build a simple web app. From here, you should get an idea on how to proceed with your own Noir projects! -Follow the instructions on [this page](https://github.com/AztecProtocol/aztec-packages/tree/master/barretenberg/cpp/src/barretenberg/bb#installation) to install `bb`. -Version 0.41.0 is compatible with `nargo` version 0.31.0, which you can install with `bbup -v 0.41.0` once `bbup` is installed. - -Easy enough. Onwards! - -## Our project - -ZK is a powerful technology. An app that doesn't reveal one of the inputs to _anyone_ is almost unbelievable, yet Noir makes it as easy as a single line of code. - -In fact, it's so simple that it comes nicely packaged in `nargo`. Let's do that! +You can find the complete app code for this guide [here](https://github.com/noir-lang/tiny-noirjs-app). -### Nargo +## Dependencies -Run: +Before we start, we want to make sure we have Node installed. For convenience (and speed), we can just install [Bun](https://bun.sh) as our package manager, and Node will work out-of-the-box: ```bash -nargo new circuit +curl -fsSL https://bun.sh/install | bash ``` -And... That's about it. Your program is ready to be compiled and run. +Let's go barebones. Doing the bare minimum is not only simple, but also allows you to easily adapt it to almost any frontend framework. -To compile, let's `cd` into the `circuit` folder to enter our project, and call: +Barebones means we can immediately start with the dependencies even on an empty folder 😈: ```bash -nargo compile +bun i @noir-lang/noir_wasm@1.0.0-beta.0 @noir-lang/noir_js@1.0.0-beta.0 @aztec/bb.js@0.63.1 ``` -This compiles our circuit into `json` format and add it to a new `target` folder. +Wait, what are these dependencies? -:::info +- `noir_wasm` is the `wasm` version of the Noir compiler. Although most developers prefer to use `nargo` for compiling, there's nothing wrong with `noir_wasm`. We like `noir_wasm`. +- `noir_js` is the main Noir package. It will execute our program, and generate the witness that will be sent to the backend. +- `bb.js` is the Typescript interface for Aztec's Barretenberg proving backend. It also uses the `wasm` version in order to run on the browser. -At this point in the tutorial, your folder structure should look like this: +:::info -```tree -. -└── circuit <---- our working directory - β”œβ”€β”€ Nargo.toml - β”œβ”€β”€ src - β”‚ └── main.nr - └── target - └── circuit.json -``` +In this guide, we will install versions pinned to 1.0.0-beta.0. These work with Barretenberg version 0.63.1, so we are using that one version too. Feel free to try with older or later versions, though! ::: -### Node and Vite - -If you want to explore Nargo, feel free to go on a side-quest now and follow the steps in the -[getting started](../getting_started/quick_start.md) guide. However, we want our app to run on the browser, so we need Vite. +## Setting up our Noir program -Vite is a powerful tool to generate static websites. While it provides all kinds of features, let's just go barebones with some good old vanilla JS. +ZK is a powerful technology. An app that reveals computational correctness but doesn't reveal some of its inputs is almost unbelievable, yet Noir makes it as easy as a single line of code. -To do this this, go back to the previous folder (`cd ..`) and create a new vite project by running `npm create vite` and choosing "Vanilla" and "Javascript". +:::tip -A wild `vite-project` directory should now appear in your root folder! Let's not waste any time and dive right in: +It's not just you. We also enjoy syntax highlighting. [Check out the Language Server](../tooling/language_server.md) -```bash -cd vite-project -``` +::: -### Setting Up Vite and Configuring the Project - -Before we proceed with any coding, let's get our environment tailored for Noir. We'll start by laying down the foundations with a `vite.config.js` file. This little piece of configuration is our secret sauce for making sure everything meshes well with the NoirJS libraries and other special setups we might need, like handling WebAssembly modules. Here’s how you get that going: - -#### Creating the vite.config.js - -In your freshly minted `vite-project` folder, create a new file named `vite.config.js` and open it in your code editor. Paste the following to set the stage: - -```javascript -import { defineConfig } from 'vite'; -import copy from 'rollup-plugin-copy'; -import fs from 'fs'; -import path from 'path'; - -const wasmContentTypePlugin = { - name: 'wasm-content-type-plugin', - configureServer(server) { - server.middlewares.use(async (req, res, next) => { - if (req.url.endsWith('.wasm')) { - res.setHeader('Content-Type', 'application/wasm'); - const newPath = req.url.replace('deps', 'dist'); - const targetPath = path.join(__dirname, newPath); - const wasmContent = fs.readFileSync(targetPath); - return res.end(wasmContent); - } - next(); - }); - }, -}; +All you need is a `main.nr` and a `Nargo.toml` file. You can follow the [noirup](../getting_started/noir_installation.md) installation and just run `noirup -v 1.0.0-beta.0`, or just create them by hand: -export default defineConfig(({ command }) => { - if (command === 'serve') { - return { - build: { - target: 'esnext', - rollupOptions: { - external: ['@aztec/bb.js'] - } - }, - optimizeDeps: { - esbuildOptions: { - target: 'esnext' - } - }, - plugins: [ - copy({ - targets: [{ src: 'node_modules/**/*.wasm', dest: 'node_modules/.vite/dist' }], - copySync: true, - hook: 'buildStart', - }), - command === 'serve' ? wasmContentTypePlugin : [], - ], - }; - } - - return {}; -}); +```bash +mkdir -p circuit/src +touch circuit/src/main.nr circuit/Nargo.toml ``` -#### Install Dependencies - -Now that our stage is set, install the necessary NoirJS packages along with our other dependencies: +To make our program interesting, let's give it a real use-case scenario: Bob wants to prove he is older than 18, without disclosing his age. Open `main.nr` and write: -```bash -npm install && npm install @noir-lang/backend_barretenberg@0.31.0 @noir-lang/noir_js@0.31.0 -npm install rollup-plugin-copy --save-dev +```rust +fn main(age: u8) { + assert(age >= 18); +} ``` -:::info - -At this point in the tutorial, your folder structure should look like this: +This program accepts a private input called age, and simply proves this number is higher than 18. But to run this code, we need to give the compiler a `Nargo.toml` with at least a name and a type: -```tree -. -└── circuit - └── ...etc... -└── vite-project <---- our working directory - └── ...etc... +```toml +[package] +name = "circuit" +type = "bin" ``` -::: +This is all that we need to get started with Noir. -#### Some cleanup +![my heart is ready for you, noir.js](@site/static/img/memes/titanic.jpeg) -`npx create vite` is amazing but it creates a bunch of files we don't really need for our simple example. Actually, let's just delete everything except for `vite.config.js`, `index.html`, `main.js` and `package.json`. I feel lighter already. +## Setting up our app -![my heart is ready for you, noir.js](@site/static/img/memes/titanic.jpeg) +Remember when apps only had one `html` and one `js` file? Well, that's enough for Noir webapps. Let's create them: -## HTML +```bash +touch index.html index.js +``` -Our app won't run like this, of course. We need some working HTML, at least. Let's open our broken-hearted `index.html` and replace everything with this code snippet: +And add something useful to our HTML file: ```html @@ -200,11 +105,11 @@ Our app won't run like this, of course. We need some working HTML, at least. Let - +

Noir app

- - + +

Logs

@@ -214,32 +119,26 @@ Our app won't run like this, of course. We need some working HTML, at least. Let ``` -It _could_ be a beautiful UI... Depending on which universe you live in. - -## Some good old vanilla Javascript - -Our love for Noir needs undivided attention, so let's just open `main.js` and delete everything (this is where the romantic scenery becomes a bit creepy). +It _could_ be a beautiful UI... Depending on which universe you live in. In any case, we're using some scary CSS to make two boxes that will show cool things on the screen. -Start by pasting in this boilerplate code: +As for the JS, real madmen could just `console.log` everything, but let's say we want to see things happening (the true initial purpose of JS... right?). Here's some boilerplate for that. Just paste it in `index.js`: ```js -function display(container, msg) { - const c = document.getElementById(container); - const p = document.createElement('p'); - p.textContent = msg; - c.appendChild(p); -} +const show = (id, content) => { + const container = document.getElementById(id); + container.appendChild(document.createTextNode(content)); + container.appendChild(document.createElement("br")); +}; -document.getElementById('submitGuess').addEventListener('click', async () => { - try { - // here's where love happens - } catch (err) { - display('logs', 'Oh πŸ’” Wrong guess'); - } +document.getElementById("submit").addEventListener("click", async () => { + try { + // noir goes here + } catch { + show("logs", "Oh πŸ’”"); + } }); -``` -The display function doesn't do much. We're simply manipulating our website to see stuff happening. For example, if the proof fails, it will simply log a broken heart 😒 +``` :::info @@ -248,30 +147,56 @@ At this point in the tutorial, your folder structure should look like this: ```tree . └── circuit - └── ...same as above -└── vite-project - β”œβ”€β”€ vite.config.js - β”œβ”€β”€ main.js - β”œβ”€β”€ package.json - └── index.html + └── src + └── main.nr + Nargo.toml + index.js + package.json + index.html + ...etc ``` -You'll see other files and folders showing up (like `package-lock.json`, `node_modules`) but you shouldn't have to care about those. - ::: -## Some NoirJS +## Compile compile compile -We're starting with the good stuff now. If you've compiled the circuit as described above, you should have a `json` file we want to import at the very top of our `main.js` file: +Finally we're up for something cool. But before we can execute a Noir program, we need to compile it into ACIR: an abstract representation. Here's where `noir_wasm` comes in. -```ts -import circuit from '../circuit/target/circuit.json'; +`noir_wasm` expects a filesystem so it can resolve dependencies. While we could use the `public` folder, let's just import those using the nice `?url` syntax provided by vite. At the top of the file: + +```js +import { compile, createFileManager } from "@noir-lang/noir_wasm" + +import main from "./circuit/src/main.nr?url"; +import nargoToml from "./circuit/Nargo.toml?url"; ``` -[Noir is backend-agnostic](../index.mdx#whats-new-about-noir). We write Noir, but we also need a proving backend. That's why we need to import and instantiate the two dependencies we installed above: `BarretenbergBackend` and `Noir`. Let's import them right below: +Compiling on the browser is common enough that `createFileManager` already gives us a nice in-memory filesystem we can use. So all we need to compile is fetching these files, writing them to our filesystem, and compile. Add this function: ```js -import { BarretenbergBackend, BarretenbergVerifier as Verifier } from '@noir-lang/backend_barretenberg'; +export async function getCircuit() { + const fm = createFileManager("/"); + const { body } = await fetch(main); + const { body: nargoTomlBody } = await fetch(nargoToml); + + fm.writeFile("./src/main.nr", body); + fm.writeFile("./Nargo.toml", nargoTomlBody); + return await compile(fm); +} +``` + +:::tip + +As you can imagine, with `node` it's all conveniently easier since you get native access to `fs`... + +::: + +## Some more JS + +We're starting with the good stuff now. We want to execute our circuit to get the witness, and then feed that witness to Barretenberg. Luckily, both packages are quite easy to work with. Let's import them at the top of the file: + +```js +import { UltraHonkBackend } from '@aztec/bb.js'; import { Noir } from '@noir-lang/noir_js'; ``` @@ -279,88 +204,103 @@ And instantiate them inside our try-catch block: ```ts // try { -const backend = new BarretenbergBackend(circuit); -const noir = new Noir(circuit); +const { program } = await getCircuit(); +const noir = new Noir(program); +const backend = new UltraHonkBackend(program.bytecode); // } ``` -:::note +:::warning -For the remainder of the tutorial, everything will be happening inside the `try` block +WASMs are not always easy to work with. In our case, `vite` likes serving them with the wrong MIME type. There are different fixes but we found the easiest one is just YOLO instantiating the WASMs manually. Paste this at the top of the file, just below the other imports, and it will work just fine: + +```js +import initNoirC from "@noir-lang/noirc_abi"; +import initACVM from "@noir-lang/acvm_js"; +import acvm from "@noir-lang/acvm_js/web/acvm_js_bg.wasm?url"; +import noirc from "@noir-lang/noirc_abi/web/noirc_abi_wasm_bg.wasm?url"; +await Promise.all([initACVM(fetch(acvm)), initNoirC(fetch(noirc))]); +``` ::: -## Our app +## Executing and proving -Now for the app itself. We're capturing whatever is in the input when people press the submit button. Just add this: +Now for the app itself. We're capturing whatever is in the input when people press the submit button. Inside our `try` block, let's just grab that input and get its value. Noir will gladly execute it, and give us a witness: ```js -const x = parseInt(document.getElementById('guessInput').value); -const input = { x, y: 2 }; +const age = document.getElementById("age").value; +show("logs", "Generating witness... ⏳"); +const { witness } = await noir.execute({ age }); +show("logs", "Generated witness... βœ…"); + ``` +:::note + +For the remainder of the tutorial, everything will be happening inside the `try` block + +::: + Now we're ready to prove stuff! Let's feed some inputs to our circuit and calculate the proof: ```js -await setup(); // let's squeeze our wasm inits here - -display('logs', 'Generating proof... βŒ›'); -const { witness } = await noir.execute(input); +show("logs", "Generating proof... ⏳"); const proof = await backend.generateProof(witness); -display('logs', 'Generating proof... βœ…'); -display('results', proof.proof); +show("logs", "Generated proof... βœ…"); +show("results", proof.proof); ``` -You're probably eager to see stuff happening, so go and run your app now! +Our program is technically **done** . You're probably eager to see stuff happening! To serve this in a convenient way, we can use a bundler like `vite` by creating a `vite.config.js` file: + +```bash +touch vite.config.js +``` -From your terminal, run `npm run dev`. If it doesn't open a browser for you, just visit `localhost:5173`. You should now see the worst UI ever, with an ugly input. +`vite` helps us with a little catch: `bb.js` in particular uses top-level awaits which aren't supported everywhere. So we can add this to the `vite.config.js` to make the bundler optimize them: -![Getting Started 0](@site/static/img/noir_getting_started_1.png) +```js +export default { optimizeDeps: { esbuildOptions: { target: "esnext" } } }; +``` + +This should be enough for vite. We don't even need to install it, just run: + +```bash +bunx vite +``` -Now, our circuit says `fn main(x: Field, y: pub Field)`. This means only the `y` value is public, and it's hardcoded above: `input = { x, y: 2 }`. In other words, you won't need to send your secret`x` to the verifier! +If it doesn't open a browser for you, just visit `localhost:5173`. You should now see the worst UI ever, with an ugly input. -By inputting any number other than 2 in the input box and clicking "submit", you should get a valid proof. Otherwise the proof won't even generate correctly. By the way, if you're human, you shouldn't be able to understand anything on the "proof" box. That's OK. We like you, human ❀️. +![Noir Webapp UI](@site/static/img/tutorials/noirjs_webapp/webapp1.png) + +Now, our circuit requires a private input `fn main(age: u8)`, and fails if it is less than 18. Let's see if it works. Submit any number above 18 (as long as it fits in 8 bits) and you should get a valid proof. Otherwise the proof won't even generate correctly. + +By the way, if you're human, you shouldn't be able to understand anything on the "proof" box. That's OK. We like you, human ❀️. ## Verifying Time to celebrate, yes! But we shouldn't trust machines so blindly. Let's add these lines to see our proof being verified: ```js -display('logs', 'Verifying proof... βŒ›'); +show('logs', 'Verifying proof... βŒ›'); const isValid = await backend.verifyProof(proof); - -// or to cache and use the verification key: -// const verificationKey = await backend.getVerificationKey(); -// const verifier = new Verifier(); -// const isValid = await verifier.verifyProof(proof, verificationKey); - -if (isValid) display('logs', 'Verifying proof... βœ…'); +show("logs", `Proof is ${isValid ? "valid" : "invalid"}... βœ…`); ``` You have successfully generated a client-side Noir web app! ![coded app without math knowledge](@site/static/img/memes/flextape.jpeg) -## Further Reading - -You can see how noirjs is used in a full stack Next.js hardhat application in the [noir-starter repo here](https://github.com/noir-lang/noir-starter/tree/main/vite-hardhat). The example shows how to calculate a proof in the browser and verify it with a deployed Solidity verifier contract from noirjs. - -You should also check out the more advanced examples in the [noir-examples repo](https://github.com/noir-lang/noir-examples), where you'll find reference usage for some cool apps. +## Next steps -## UltraHonk Backend +At this point, you have a working ZK app that works on the browser. Actually, it works on a mobile phone too! -Barretenberg has recently exposed a new UltraHonk backend. We can use UltraHonk in NoirJS after version 0.33.0. Everything will be the same as the tutorial above, except that the class we need to import will change: +If you want to continue learning by doing, here are some challenges for you: -```js -import { UltraHonkBackend, UltraHonkVerifier as Verifier } from '@noir-lang/backend_barretenberg'; -``` - -The backend will then be instantiated as such: - -```js -const backend = new UltraHonkBackend(circuit); -``` +- Install [nargo](https://noir-lang.org/docs/getting_started/noir_installation) and write [Noir tests](../tooling/testing) +- Change the circuit to accept a [public input](../noir/concepts/data_types/#private--public-types) as the cutoff age. It could be different depending on the purpose, for example! +- Enjoy Noir's Rust-like syntax and write a struct `Country` that implements a trait `MinAge` with a method `get_min_age`. Then, make a struct `Person` have an `u8` as its age and a country of type `Country`. You can pass a `person` in JS just like a JSON object `person: { age, country: { min_age: 18 }}` -Then all the commands to prove and verify your circuit will be same. +The world is your stage, just have fun with ZK! You can see how noirjs is used in a full stack Next.js hardhat application in the [noir-starter repo here](https://github.com/noir-lang/noir-starter/tree/main/vite-hardhat). The example shows how to calculate a proof in the browser and verify it with a deployed Solidity verifier contract from noirjs. -The only feature currently unsupported with UltraHonk are [recursive proofs](../explainers/explainer-recursion.md). +Check out other starters, tools, or just cool projects in the [awesome noir repository](https://github.com/noir-lang/awesome-noir).