Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-generate a ContractInterface struct (in Noir) for a Noir Contract #1237

Closed
iAmMichaelConnor opened this issue Jul 27, 2023 · 0 comments · Fixed by #1487
Closed

Auto-generate a ContractInterface struct (in Noir) for a Noir Contract #1237

iAmMichaelConnor opened this issue Jul 27, 2023 · 0 comments · Fixed by #1487
Assignees
Labels
T-feature-request Type: Adding a brand new feature (not to be confused with improving an existing feature).

Comments

@iAmMichaelConnor
Copy link
Contributor

iAmMichaelConnor commented Jul 27, 2023

The current syntax for making an external function call is clunky.

We should update the syntax to feel more like Solidity: import a ContractInterface, create an instance of the contract at an address, then make a call to a named method of the contract.

Basically this:

    let b_address = 1234;
    let b = B_ContractInterface::at(b_address);

    let (return_value, mut context_1) = b.foo(context, point, field, boolean); // call foo :)

I've written an example ContractInterface for some contract B (contract B itself is not shown here), which I believe could be auto-generated as its own b-contract-interface.nr file. @spalladino has written some code already to auto-generate a contract interface which can be consumed by typescript code. This issue seeks something similar, but a contract interface which can be consumed by other noir contracts.

TODO:

  • Ask @vezenovm to implement a 'string to bytes' method :) (because the below example only works with a hack to Noir) We won't need this if generating the contract interface programmatically. We can hard-code the function selectors into the auto-generated interface.
  • Test how to make Noir keccak equal to Typescript Noir keccak (what I mean by this is: ensure the function selectors match in Noir and TS worlds). We won't need this if generating the contract interface programmatically. We can hard-code the function selectors into the auto-generated interface.
  • Ask the Noir team how we can compute an array length constant from other constants (see the TODO in the code below)
    E.g. let my_array = [0; const1 + const2];
    • Or find another way that would make this interface 'auto-generatable' for custom types.
  • Write the typescript code which can parse a contract and auto-generate its ContractInterface struct.
use dep::std;
use dep::aztec::context::Context;
use dep::aztec::private_call_stack_item::PrivateCallStackItem;
use dep::aztec::abi::PrivateContextInputs;
use dep::aztec::constants_gen;
use dep::aztec::abi;

// Spread an array into an array:
fn spread<SRC_LEN, TARGET_LEN>(mut target_arr: [Field; TARGET_LEN], src_arr: [Field; SRC_LEN], at_index: Field) -> [Field; TARGET_LEN] {
    let mut j = at_index;
    for i in 0..SRC_LEN {
        target_arr[j] = src_arr[i]; 
    }
    target_arr
}

struct Point {
    x: Field,
    y: Field
}

// Every custom type MUST implement these methods, if we want to use those types as args or return_values.
impl Point {
    // Assume the existence of these 3 serialisation methods:
    fn serialise(self) -> [Field; 2] {
        [self.x, self.y]
    }

    fn deserialise(point: [Field; 2]) -> Self {
        Point {
            x: point[0],
            y: point[1],
        }
    }

    fn serialised_length() -> Field {
        2
    }
}

fn be_bytes_to_field<LEN>(bytes: [u8; LEN]) -> Field {
    let mut f = 0;
    let mut b = 1;
    let mut len: Field = LEN - 1;
    for i in 0..LEN {
        let j = len - i;
        f += (bytes[j] as Field) * b;
        b *= 256;
    }
    f
}

// WARNING: this relies on a local hack to Noir, to make `str<LEN>` and `[u8; LEN]` interchangeable (the same).
// Maxim has kindly offered to instead create a string to bytes std lib method. 
// TODO: this should ideally be an _unconstrained_ function. We don't want constraints for a function selector calculation:
// the function selector should be a hard-coded constant.
// TODO: it would be nice if the output of an unconstrained function could become a hard-coded constant in the circuit.
// TODO: I only wrote code to compute the function selector, because my initial attempt was going to encourage devs to 
// write this interface themselves, and so a human readable function signature string "foo(...)" was more human friendly
// than requiring devs to derive function selectors themselves. BUT - if we're opting for an approach of auto-generating
// the interface, then we can auto-generate the function selectors and hard-code them too!!!
fn compute_function_selector<LEN>(function_signature: [u8; LEN]) -> Field {
    let msg_len: u32 = LEN;
    be_bytes_to_field(std::hash::keccak256(function_signature, msg_len))
}

// EVERYTHING ABOVE THIS SHOULD LIVE IN OTHER NOIR-LIB FILES.

struct B_ContractInterface {
    address: Field,
}

impl B_ContractInterface {
    fn at(address: Field) -> Self {
        Self {
            address,
        }
    }

    fn call<ARGS_LEN, SEL_LEN>(self, mut context: Context, signature: [u8; SEL_LEN], args: [Field; ARGS_LEN]) -> ([Field; constants_gen::RETURN_VALUES_LENGTH], Context) {
        let selector = compute_function_selector(signature);
        let (callStackItem, mut context_1) = PrivateCallStackItem::call(self.address, selector, args, context);
        (callStackItem.public_inputs.return_values, context_1)
    }

// Everything above this is boilerplate ***********************************************************************************

    // When creating the contract's interface, the dev has to:
    // - Specify how to serialise the args
    // - Specify the function signature (the string "foo(field,field)")
    fn foo(self, mut context: Context, point: Point, field: Field, boolean: bool) -> (Field, Context) {

        // Serialisation boilerplate.
        // Serialising the arguments in a way which can probably be done programmatically by a typescript code generator.
        // TODO: I think we need a way of cumputing constants from other constants, to make this work.
        // let serialised_args_length = Point::serialised_length() + 1 + 1; // Can't do this until we have comptime arithmetic.
        let mut serialised_args = [0; 4]; // Unfortunately need hard-coded 4 here, instead of using serialised_args_length. :(

        let mut spread_index: Field = 0;
        serialised_args = spread(serialised_args, point.serialise(), spread_index);
        spread_index += Point::serialised_length();

        serialised_args = spread(serialised_args, [field], spread_index);
        spread_index += 1;

        serialised_args = spread(serialised_args, [boolean as Field], spread_index);
        // End of serialisation boilerplate

        // 
        let (return_values, mut context_1) = self.call(context, "foo(struct,field,boolean)", serialised_args); // TODO: how are we encoding a struct when defining a function signature?
        
        (return_values[0], context_1)
    }
}


// I've been a bit lazy here, and am using vanilla noir, rather than putting the function in a `contract` scope.
fn main(
    inputs: PrivateContextInputs,
    point: Point,
    field: Field,
    boolean: bool
) -> pub Field {
    let mut context = Context::new(inputs, abi::hash_args([point.x, point.y, field, boolean as Field]));

    let b_address = 1234;
    let b = B_ContractInterface::at(b_address);

    let (return_value, mut context_1) = b.foo(context, point, field, boolean); // call foo :)
    return_value
}
@iAmMichaelConnor iAmMichaelConnor added the T-feature-request Type: Adding a brand new feature (not to be confused with improving an existing feature). label Jul 27, 2023
@github-project-automation github-project-automation bot moved this to Todo in A3 Jul 27, 2023
@spalladino spalladino moved this from Todo to In Review in A3 Aug 9, 2023
spalladino added a commit that referenced this issue Aug 14, 2023
… contracts (#1487)

Builds on @iAmMichaelConnor work to generate a contract interface for
simplifying calling function in other contracts. Uses only information
present in the ABI. For each function in the target contract, creates a
wrapper function that receives the same arguments, serialises them based
on the standard ABI encoding format (see
[here](https://github.com/AztecProtocol/aztec-packages/blob/49d272159f1b27521ad34081c7f1622ccac19dff/yarn-project/foundation/src/abi/encoder.ts)),
and uses the `call_private_function` from the `context` to call into
them.

To handle custom structs, we're re-defining them in the contract
interface. Until we get struct type names in the ABI (see
noir-lang/noir#2238), we are autogenerating
the name as well, based on the function name and param name where they
are used. Serialisation is done manually in the code, but we may want to
replace it with a Noir intrinsic when available (see
noir-lang/noir#2240).

See [this
file](https://github.com/AztecProtocol/aztec-packages/blob/49d272159f1b27521ad34081c7f1622ccac19dff/yarn-project/noir-contracts/src/contracts/test_contract/src/test_contract_interface.nr)
for example output.

Fixes #1237

---------

Co-authored-by: iAmMichaelConnor <[email protected]>
@github-project-automation github-project-automation bot moved this from In Review to Done in A3 Aug 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-feature-request Type: Adding a brand new feature (not to be confused with improving an existing feature).
Projects
Archived in project
2 participants