Skip to content

Commit

Permalink
feat(avm): add AvmContextInputs (AztecProtocol/aztec-packages#5396)
Browse files Browse the repository at this point in the history
NOTE: I don't know why this triggered a change in the snapshot.

---

This structure lets us easily pass things from the (TS) context to the constructor of the `AvmContext` in Noir, using `calldata` as a vehicle (effectively adding them as public inputs).

The choice to add the structure to the function arguments as LAST and not first is because adding them first would break any non-noir-generated bytecode (since they would have to shift their expected calldata by a magic number `N = sizeof(AvmContextInputs)`). Putting it last, makes it transparent for them. A calldatacopy would still work.

However, having this makes any external call always have `AvmContextInputs` in the calldata, bloating it (on chain) for non-noir-generated bytecode. This is not an issue now.

For the moment, this is temporary, but might be useful long term. (Sean mentioned passing maybe env getters like this).

---

Implemented `AvmContext.selector()` and `AvmContext.get_args_hash()` using the above.
  • Loading branch information
AztecBot committed Mar 25, 2024
1 parent 13eb71b commit 27fce09
Show file tree
Hide file tree
Showing 17 changed files with 132 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .aztec-sync-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
208abbb63af4c9a3f25d723fe1c49e82aa461061
12e2844f9af433beb1a586640b08ce284ad91095
2 changes: 1 addition & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Resolves <!-- Link to GitHub Issue -->
Check one:
- [ ] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[Exceptional Case]** Documentation to be submitted in a separate PR.
- [ ] **[For Experimental Features]** Documentation to be submitted in a separate PR.

# PR Checklist\*

Expand Down
126 changes: 66 additions & 60 deletions aztec_macros/src/transforms/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ pub fn transform_vm_function(
let create_context = create_avm_context()?;
func.def.body.0.insert(0, create_context);

// Add the inputs to the params (first!)
let input = create_inputs("AvmContextInputs");
func.def.parameters.insert(0, input);

// We want the function to be seen as a public function
func.def.is_unconstrained = true;

Expand Down Expand Up @@ -232,62 +236,62 @@ fn create_assert_initializer() -> Statement {
/// ```noir
/// #[aztec(private)]
/// fn foo(structInput: SomeStruct, arrayInput: [u8; 10], fieldInput: Field) -> Field {
/// // Create the hasher object
/// let mut hasher = Hasher::new();
/// // Create the bounded vec object
/// let mut serialized_args = BoundedVec::new();
///
/// // struct inputs call serialize on them to add an array of fields
/// hasher.add_multiple(structInput.serialize());
/// serialized_args.extend_from_array(structInput.serialize());
///
/// // Array inputs are iterated over and each element is added to the hasher (as a field)
/// // Array inputs are iterated over and each element is added to the bounded vec (as a field)
/// for i in 0..arrayInput.len() {
/// hasher.add(arrayInput[i] as Field);
/// serialized_args.push(arrayInput[i] as Field);
/// }
/// // Field inputs are added to the hasher
/// hasher.add({ident});
/// // Field inputs are added to the bounded vec
/// serialized_args.push({ident});
///
/// // Create the context
/// // The inputs (injected by this `create_inputs`) and completed hash object are passed to the context
/// let mut context = PrivateContext::new(inputs, hasher.hash());
/// let mut context = PrivateContext::new(inputs, hash_args(serialized_args));
/// }
/// ```
fn create_context(ty: &str, params: &[Param]) -> Result<Vec<Statement>, AztecMacroError> {
let mut injected_expressions: Vec<Statement> = vec![];

// `let mut hasher = Hasher::new();`
let let_hasher = mutable_assignment(
"hasher", // Assigned to
// `let mut serialized_args = BoundedVec::new();`
let let_serialized_args = mutable_assignment(
"serialized_args", // Assigned to
call(
variable_path(chained_dep!("aztec", "hasher", "Hasher", "new")), // Path
vec![], // args
variable_path(chained_dep!("std", "collections", "bounded_vec", "BoundedVec", "new")), // Path
vec![], // args
),
);

// Completes: `let mut hasher = Hasher::new();`
injected_expressions.push(let_hasher);
// Completes: `let mut serialized_args = BoundedVec::new();`
injected_expressions.push(let_serialized_args);

// Iterate over each of the function parameters, adding to them to the hasher
// Iterate over each of the function parameters, adding to them to the bounded vec
for Param { pattern, typ, span, .. } in params {
match pattern {
Pattern::Identifier(identifier) => {
// Match the type to determine the padding to do
let unresolved_type = &typ.typ;
let expression = match unresolved_type {
// `hasher.add_multiple({ident}.serialize())`
UnresolvedTypeData::Named(..) => add_struct_to_hasher(identifier),
// `serialized_args.extend_from_array({ident}.serialize())`
UnresolvedTypeData::Named(..) => add_struct_to_serialized_args(identifier),
UnresolvedTypeData::Array(_, arr_type) => {
add_array_to_hasher(identifier, arr_type)
add_array_to_serialized_args(identifier, arr_type)
}
// `hasher.add({ident})`
UnresolvedTypeData::FieldElement => add_field_to_hasher(identifier),
// Add the integer to the hasher, casted to a field
// `hasher.add({ident} as Field)`
// `serialized_args.push({ident})`
UnresolvedTypeData::FieldElement => add_field_to_serialized_args(identifier),
// Add the integer to the serialized args, casted to a field
// `serialized_args.push({ident} as Field)`
UnresolvedTypeData::Integer(..) | UnresolvedTypeData::Bool => {
add_cast_to_hasher(identifier)
add_cast_to_serialized_args(identifier)
}
UnresolvedTypeData::String(..) => {
let (var_bytes, id) = str_to_bytes(identifier);
injected_expressions.push(var_bytes);
add_array_to_hasher(
add_array_to_serialized_args(
&id,
&UnresolvedType {
typ: UnresolvedTypeData::Integer(
Expand All @@ -313,11 +317,10 @@ fn create_context(ty: &str, params: &[Param]) -> Result<Vec<Statement>, AztecMac

// Create the inputs to the context
let inputs_expression = variable("inputs");
// `hasher.hash()`
let hash_call = method_call(
variable("hasher"), // variable
"hash", // method name
vec![], // args
// `hash_args(serialized_args)`
let hash_call = call(
variable_path(chained_dep!("aztec", "hash", "hash_args")), // variable
vec![variable("serialized_args")], // args
);

let path_snippet = ty.to_case(Case::Snake); // e.g. private_context
Expand Down Expand Up @@ -354,11 +357,14 @@ fn create_context(ty: &str, params: &[Param]) -> Result<Vec<Statement>, AztecMac
/// // ...
/// }
fn create_avm_context() -> Result<Statement, AztecMacroError> {
// Create the inputs to the context
let inputs_expression = variable("inputs");

let let_context = mutable_assignment(
"context", // Assigned to
call(
variable_path(chained_dep!("aztec", "context", "AVMContext", "new")), // Path
vec![], // args
vec![inputs_expression], // args
),
);

Expand Down Expand Up @@ -598,21 +604,21 @@ fn create_context_finish() -> Statement {
}

//
// Methods to create hasher inputs
// Methods to create hash_args inputs
//

fn add_struct_to_hasher(identifier: &Ident) -> Statement {
// If this is a struct, we call serialize and add the array to the hasher
fn add_struct_to_serialized_args(identifier: &Ident) -> Statement {
// If this is a struct, we call serialize and add the array to the serialized args
let serialized_call = method_call(
variable_path(path(identifier.clone())), // variable
"serialize", // method name
vec![], // args
);

make_statement(StatementKind::Semi(method_call(
variable("hasher"), // variable
"add_multiple", // method name
vec![serialized_call], // args
variable("serialized_args"), // variable
"extend_from_array", // method name
vec![serialized_call], // args
)))
}

Expand All @@ -632,7 +638,7 @@ fn str_to_bytes(identifier: &Ident) -> (Statement, Ident) {
}

fn create_loop_over(var: Expression, loop_body: Vec<Statement>) -> Statement {
// If this is an array of primitive types (integers / fields) we can add them each to the hasher
// If this is an array of primitive types (integers / fields) we can add them each to the serialized args
// casted to a field
let span = var.span;

Expand All @@ -644,7 +650,7 @@ fn create_loop_over(var: Expression, loop_body: Vec<Statement>) -> Statement {
);

// What will be looped over
// - `hasher.add({ident}[i] as Field)`
// - `serialized_args.push({ident}[i] as Field)`
let for_loop_block = expression(ExpressionKind::Block(BlockExpression(loop_body)));

// `for i in 0..{ident}.len()`
Expand All @@ -662,66 +668,66 @@ fn create_loop_over(var: Expression, loop_body: Vec<Statement>) -> Statement {
}))
}

fn add_array_to_hasher(identifier: &Ident, arr_type: &UnresolvedType) -> Statement {
// If this is an array of primitive types (integers / fields) we can add them each to the hasher
fn add_array_to_serialized_args(identifier: &Ident, arr_type: &UnresolvedType) -> Statement {
// If this is an array of primitive types (integers / fields) we can add them each to the serialized_args
// casted to a field

// Wrap in the semi thing - does that mean ended with semi colon?
// `hasher.add({ident}[i] as Field)`
// `serialized_args.push({ident}[i] as Field)`

let arr_index = index_array(identifier.clone(), "i");
let (add_expression, hasher_method_name) = match arr_type.typ {
let (add_expression, vec_method_name) = match arr_type.typ {
UnresolvedTypeData::Named(..) => {
let hasher_method_name = "add_multiple".to_owned();
let vec_method_name = "extend_from_array".to_owned();
let call = method_call(
// All serialize on each element
arr_index, // variable
"serialize", // method name
vec![], // args
);
(call, hasher_method_name)
(call, vec_method_name)
}
_ => {
let hasher_method_name = "add".to_owned();
let vec_method_name = "push".to_owned();
let call = cast(
arr_index, // lhs - `ident[i]`
UnresolvedTypeData::FieldElement, // cast to - `as Field`
);
(call, hasher_method_name)
(call, vec_method_name)
}
};

let block_statement = make_statement(StatementKind::Semi(method_call(
variable("hasher"), // variable
&hasher_method_name, // method name
variable("serialized_args"), // variable
&vec_method_name, // method name
vec![add_expression],
)));

create_loop_over(variable_ident(identifier.clone()), vec![block_statement])
}

fn add_field_to_hasher(identifier: &Ident) -> Statement {
// `hasher.add({ident})`
fn add_field_to_serialized_args(identifier: &Ident) -> Statement {
// `serialized_args.push({ident})`
let ident = variable_path(path(identifier.clone()));
make_statement(StatementKind::Semi(method_call(
variable("hasher"), // variable
"add", // method name
vec![ident], // args
variable("serialized_args"), // variable
"push", // method name
vec![ident], // args
)))
}

fn add_cast_to_hasher(identifier: &Ident) -> Statement {
// `hasher.add({ident} as Field)`
fn add_cast_to_serialized_args(identifier: &Ident) -> Statement {
// `serialized_args.push({ident} as Field)`
// `{ident} as Field`
let cast_operation = cast(
variable_path(path(identifier.clone())), // lhs
UnresolvedTypeData::FieldElement, // rhs
);

// `hasher.add({ident} as Field)`
// `serialized_args.push({ident} as Field)`
make_statement(StatementKind::Semi(method_call(
variable("hasher"), // variable
"add", // method name
vec![cast_operation], // args
variable("serialized_args"), // variable
"push", // method name
vec![cast_operation], // args
)))
}
9 changes: 5 additions & 4 deletions compiler/noirc_evaluator/src/ssa/ir/instruction/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ pub(super) fn simplify_call(
}
}
Intrinsic::AsSlice => {
let slice = dfg.get_array_constant(arguments[0]);
if let Some((slice, element_type)) = slice {
let slice_length = dfg.make_constant(slice.len().into(), Type::length_type());
let new_slice = dfg.make_array(slice, element_type);
let array = dfg.get_array_constant(arguments[0]);
if let Some((array, array_type)) = array {
let slice_length = dfg.make_constant(array.len().into(), Type::length_type());
let inner_element_types = array_type.element_types();
let new_slice = dfg.make_array(array, Type::Slice(inner_element_types));
SimplifyResult::SimplifiedToMultiple(vec![slice_length, new_slice])
} else {
SimplifyResult::None
Expand Down
7 changes: 7 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ impl Type {
Type::Reference(element) => element.contains_an_array(),
}
}

pub(crate) fn element_types(self) -> Rc<Vec<Type>> {
match self {
Type::Array(element_types, _) | Type::Slice(element_types) => element_types,
other => panic!("element_types: Expected array or slice, found {other}"),
}
}
}

/// Composite Types are essentially flattened struct or tuple types.
Expand Down
5 changes: 4 additions & 1 deletion compiler/noirc_frontend/src/hir/type_check/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ pub enum TypeCheckError {
method_name: String,
span: Span,
},
#[error("Strings do not support indexed assignment")]
StringIndexAssign { span: Span },
}

impl TypeCheckError {
Expand Down Expand Up @@ -237,7 +239,8 @@ impl From<TypeCheckError> for Diagnostic {
| TypeCheckError::ConstrainedReferenceToUnconstrained { span }
| TypeCheckError::UnconstrainedReferenceToConstrained { span }
| TypeCheckError::UnconstrainedSliceReturnToConstrained { span }
| TypeCheckError::NonConstantSliceLength { span } => {
| TypeCheckError::NonConstantSliceLength { span }
| TypeCheckError::StringIndexAssign { span } => {
Diagnostic::simple_error(error.to_string(), String::new(), span)
}
TypeCheckError::PublicReturnType { typ, span } => Diagnostic::simple_error(
Expand Down
5 changes: 5 additions & 0 deletions compiler/noirc_frontend/src/hir/type_check/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ impl<'interner> TypeChecker<'interner> {
Type::Array(_, elem_type) => *elem_type,
Type::Slice(elem_type) => *elem_type,
Type::Error => Type::Error,
Type::String(_) => {
let (_lvalue_name, lvalue_span) = self.get_lvalue_name_and_span(&lvalue);
self.errors.push(TypeCheckError::StringIndexAssign { span: lvalue_span });
Type::Error
}
other => {
// TODO: Need a better span here
self.errors.push(TypeCheckError::TypeMismatch {
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/how_to/how-to-oracles.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ For example, if your Noir program expects the host machine to provide CPU pseudo
```js
const foreignCallHandler = (name, inputs) => crypto.randomBytes(16) // etc

await noir.generateFinalProof(inputs, foreignCallHandler)
await noir.generateProof(inputs, foreignCallHandler)
```

As one can see, in NoirJS, the [`foreignCallHandler`](../reference/NoirJS/noir_js/type-aliases/ForeignCallHandler.md) function simply means "a callback function that returns a value of type [`ForeignCallOutput`](../reference/NoirJS/noir_js/type-aliases/ForeignCallOutput.md). It doesn't have to be an RPC call like in the case for Nargo.
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/noir/concepts/data_types/integers.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ The built-in structure `U128` allows you to use 128-bit unsigned integers almost
- You cannot cast between a native integer and `U128`
- There is a higher performance cost when using `U128`, compared to a native type.

Conversion between unsigned integer types and U128 are done through the use of `from_integer` and `to_integer` functions.
Conversion between unsigned integer types and U128 are done through the use of `from_integer` and `to_integer` functions. `from_integer` also accepts the `Field` type as input.

```rust
fn main() {
Expand Down
4 changes: 2 additions & 2 deletions docs/docs/tutorials/noirjs_app.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ Now we're ready to prove stuff! Let's feed some inputs to our circuit and calcul
await setup(); // let's squeeze our wasm inits here

display('logs', 'Generating proof... ⌛');
const proof = await noir.generateFinalProof(input);
const proof = await noir.generateProof(input);
display('logs', 'Generating proof... ✅');
display('results', proof.proof);
```
Expand All @@ -264,7 +264,7 @@ Time to celebrate, yes! But we shouldn't trust machines so blindly. Let's add th
```js
display('logs', 'Verifying proof... ⌛');
const verification = await noir.verifyFinalProof(proof);
const verification = await noir.verifyProof(proof);
if (verification) display('logs', 'Verifying proof... ✅');
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ For example, if your Noir program expects the host machine to provide CPU pseudo
```js
const foreignCallHandler = (name, inputs) => crypto.randomBytes(16) // etc

await noir.generateFinalProof(inputs, foreignCallHandler)
await noir.generateProof(inputs, foreignCallHandler)
```

As one can see, in NoirJS, the [`foreignCallHandler`](../reference/NoirJS/noir_js/type-aliases/ForeignCallHandler.md) function simply means "a callback function that returns a value of type [`ForeignCallOutput`](../reference/NoirJS/noir_js/type-aliases/ForeignCallOutput.md). It doesn't have to be an RPC call like in the case for Nargo.
Expand Down
Loading

0 comments on commit 27fce09

Please sign in to comment.