Skip to content

Commit

Permalink
chore: fixed call nesting, tests and docs (#4932)
Browse files Browse the repository at this point in the history
Added missing tests and fixed context propagation for nested static
calls.

Also included docs and migration notes! @AztecProtocol/devrel
  • Loading branch information
Thunkar authored Mar 5, 2024
1 parent 6535e88 commit bd5c879
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Functions serve as the building blocks of smart contracts. Functions can be eith

For a more practical guide of using multiple types of functions, follow the [token tutorial](../../../tutorials/writing_token_contract.md).

Currently, any function is "mutable" in the sense that it might alter state. In the future, we will support static calls, similarly to EVM. A static call is essentially a call that does not alter state (it keeps state static).
Currently, any function is "mutable" in the sense that it might alter state. However, we also support support static calls, similarly to EVM. A static call is essentially a call that does not alter state (it keeps state static).

## Constructors

Expand Down
2 changes: 0 additions & 2 deletions docs/docs/developers/limitations/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ Help shape and define:
- It is a testing environment, it is insecure, unaudited and does not generate any proofs, its only for testing purposes;
- Constructors can not call nor alter public state
- The constructor is executed exclusively in private domain, WITHOUT the ability to call public functions or alter public state. This means to set initial storage values, you need to follow a pattern similar to [proxies in Ethereum](https://blog.openzeppelin.com/proxy-patterns), where you `initialize` the contract with values after it have been deployed, see [constructor](../contracts/writing_contracts/functions/write_constructor.md).
- No static nor delegate calls (see [mutability](../contracts/writing_contracts/functions/main.md)).
- These values are unused in the call-context.
- Beware that what you think of as a `view` could alter state ATM! Notably the account could alter state or re-enter whenever the account contract's `is_valid` function is called.
- `msg_sender` is currently leaking when doing private -> public calls
- The `msg_sender` will always be set, if you call a public function from the private world, the `msg_sender` will be set to the private caller's address. See [function context](../contracts/writing_contracts/functions/context.md).
Expand Down
4 changes: 0 additions & 4 deletions docs/docs/learn/concepts/accounts/authwit.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,6 @@ sequenceDiagram
The call to the account contract for checking authentication should be a static call, meaning that it cannot change state or make calls that change state. If this call is not static, it could be used to re-enter the flow and change the state of the contract.
:::

:::danger Static call currently unsupported
The current execution layer does not implement static call. So currently you will be passing along the control flow :grimacing:.
:::

:::danger Re-entries
The above flow could be re-entered at token transfer. It is mainly for show to illustrate a logic outline.
:::
Expand Down
10 changes: 10 additions & 0 deletions docs/docs/misc/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ Aztec is in full-speed development. Literally every version breaks compatibility

## 0.25.0

### [Aztec.nr] Static calls

It is now possible to perform static calls from both public and private functions. Static calls forbid any modification to the state, including L2->L1 messages or log generation. Once a static context is set through a static all, every subsequent call will also be treated as static via context propagation.

```rust
context.static_call_private_function(targetContractAddress, targetSelector, args);

context.static_call_public_function(targetContractAddress, targetSelector, args);
```

### [Aztec.nr] Introduction to `prelude`

A new `prelude` module to include common Aztec modules and types.
Expand Down
2 changes: 2 additions & 0 deletions noir-projects/aztec-nr/aztec/src/context/private_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ impl PrivateContext {
is_static_call: bool,
is_delegate_call: bool
) -> [Field; RETURN_VALUES_LENGTH] {
let mut is_static_call = is_static_call | self.inputs.call_context.is_static_call;
let item = call_private_function_internal(
contract_address,
function_selector,
Expand Down Expand Up @@ -434,6 +435,7 @@ impl PrivateContext {
is_static_call: bool,
is_delegate_call: bool
) {
let mut is_static_call = is_static_call | self.inputs.call_context.is_static_call;
let fields = enqueue_public_function_call_internal(
contract_address,
function_selector,
Expand Down
68 changes: 68 additions & 0 deletions noir-projects/noir-contracts/contracts/parent_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,39 @@ contract Parent {
return_values[0]
}

#[aztec(private)]
fn privateCall(
targetContract: AztecAddress,
targetSelector: FunctionSelector,
args: [Field; 2]
) -> Field {
// Call the target private function
let return_values = context.call_private_function(targetContract, targetSelector, args);

// Copy the return value from the call to this function's return values
return_values[0]
}

// Private function to set a static context and verify correct propagation for nested private calls
#[aztec(private)]
fn privateStaticCallNested(
targetContract: AztecAddress,
targetSelector: FunctionSelector,
args: [Field; 2]
) -> Field {
// Call the target private function statically
let privateCallSelector = FunctionSelector::from_signature("privateCall((Field),(u32),[Field;2])");
let thisAddress = context.this_address();
let return_values = context.static_call_private_function(
thisAddress,
privateCallSelector,
[targetContract.to_field(), targetSelector.to_field(), args[0], args[1]]
);

// Copy the return value from the call to this function's return values
return_values[0]
}

// Public function to directly call another public function to the targetContract using the selector and value provided
#[aztec(public)]
fn publicStaticCall(
Expand All @@ -161,6 +194,41 @@ contract Parent {
return_values[0]
}

// Public function to set a static context and verify correct propagation for nested public calls
#[aztec(public)]
fn publicNestedStaticCall(
targetContract: AztecAddress,
targetSelector: FunctionSelector,
args: [Field; 1]
) -> Field {
// Call the target public function through the pub entrypoint statically
let pubEntryPointSelector = FunctionSelector::from_signature("pubEntryPoint((Field),(u32),Field)");
let thisAddress = context.this_address();
let return_values = context.static_call_public_function(
thisAddress,
pubEntryPointSelector,
[targetContract.to_field(), targetSelector.to_field(), args[0]]
);
return_values[0]
}

// Private function to enqueue a static call to the pubEntryPoint function of another contract, passing the target arguments provided
#[aztec(private)]
fn enqueueStaticNestedCallToPubFunction(
targetContract: AztecAddress,
targetSelector: FunctionSelector,
args: [Field; 1]
) {
// Call the target public function through the pub entrypoint statically
let pubEntryPointSelector = FunctionSelector::from_signature("pubEntryPoint((Field),(u32),Field)");
let thisAddress = context.this_address();
context.static_call_public_function(
thisAddress,
pubEntryPointSelector,
[targetContract.to_field(), targetSelector.to_field(), args[0]]
);
}

// Private function to enqueue a static call to the pubEntryPoint function of another contract, passing the target arguments provided
#[aztec(private)]
fn enqueueStaticCallToPubFunction(
Expand Down
60 changes: 58 additions & 2 deletions yarn-project/end-to-end/src/e2e_static_calls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,40 @@ describe('e2e_static_calls', () => {
.wait();
}, 100_000);

it('performs legal (nested) private to private static calls', async () => {
await parentContract.methods
.privateStaticCallNested(childContract.address, childContract.methods.privateGetValue.selector, [
42n,
wallet.getCompleteAddress().address,
])
.send()
.wait();
}, 100_000);

it('performs legal public to public static calls', async () => {
await parentContract.methods
.enqueueStaticCallToPubFunction(childContract.address, childContract.methods.pubGetValue.selector, [42n])
.publicStaticCall(childContract.address, childContract.methods.pubGetValue.selector, [42n])
.send()
.wait();
}, 100_000);

it('performs legal (nested) public to public static calls', async () => {
await parentContract.methods
.publicNestedStaticCall(childContract.address, childContract.methods.pubGetValue.selector, [42n])
.send()
.wait();
}, 100_000);

it('performs legal enqueued public static calls', async () => {
await parentContract.methods
.publicStaticCall(childContract.address, childContract.methods.pubGetValue.selector, [42n])
.enqueueStaticCallToPubFunction(childContract.address, childContract.methods.pubGetValue.selector, [42n])
.send()
.wait();
}, 100_000);

it('performs legal (nested) enqueued public static calls', async () => {
await parentContract.methods
.enqueueStaticNestedCallToPubFunction(childContract.address, childContract.methods.pubGetValue.selector, [42n])
.send()
.wait();
}, 100_000);
Expand All @@ -57,6 +81,18 @@ describe('e2e_static_calls', () => {
).rejects.toThrow('Static call cannot create new notes, emit L2->L1 messages or generate logs');
}, 100_000);

it('fails when performing illegal (nested) private to private static calls', async () => {
await expect(
parentContract.methods
.privateStaticCallNested(childContract.address, childContract.methods.privateSetValue.selector, [
42n,
wallet.getCompleteAddress().address,
])
.send()
.wait(),
).rejects.toThrow('Static call cannot create new notes, emit L2->L1 messages or generate logs');
}, 100_000);

it('fails when performing illegal public to public static calls', async () => {
await expect(
parentContract.methods
Expand All @@ -66,6 +102,15 @@ describe('e2e_static_calls', () => {
).rejects.toThrow('Static call cannot update the state, emit L2->L1 messages or generate logs');
}, 100_000);

it('fails when performing illegal (nested) public to public static calls', async () => {
await expect(
parentContract.methods
.publicNestedStaticCall(childContract.address, childContract.methods.pubSetValue.selector, [42n])
.send()
.wait(),
).rejects.toThrow('Static call cannot update the state, emit L2->L1 messages or generate logs');
}, 100_000);

it('fails when performing illegal enqueued public static calls', async () => {
await expect(
parentContract.methods
Expand All @@ -74,5 +119,16 @@ describe('e2e_static_calls', () => {
.wait(),
).rejects.toThrow('Static call cannot update the state, emit L2->L1 messages or generate logs');
}, 100_000);

it('fails when performing illegal (nested) enqueued public static calls', async () => {
await expect(
parentContract.methods
.enqueueStaticNestedCallToPubFunction(childContract.address, childContract.methods.pubSetValue.selector, [
42n,
])
.send()
.wait(),
).rejects.toThrow('Static call cannot update the state, emit L2->L1 messages or generate logs');
}, 100_000);
});
});
4 changes: 4 additions & 0 deletions yarn-project/simulator/src/client/client_execution_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,8 @@ export class ClientExecutionContext extends ViewDataOracle {
`Calling private function ${this.contractAddress}:${functionSelector} from ${this.callContext.storageContractAddress}`,
);

isStaticCall = isStaticCall || this.callContext.isStaticCall;

const targetArtifact = await this.db.getFunctionArtifact(targetContractAddress, functionSelector);
const targetFunctionData = FunctionData.fromAbi(targetArtifact);

Expand Down Expand Up @@ -412,6 +414,8 @@ export class ClientExecutionContext extends ViewDataOracle {
isStaticCall: boolean,
isDelegateCall: boolean,
): Promise<PublicCallRequest> {
isStaticCall = isStaticCall || this.callContext.isStaticCall;

const targetArtifact = await this.db.getFunctionArtifact(targetContractAddress, functionSelector);
const derivedCallContext = await this.deriveCallContext(
targetContractAddress,
Expand Down
2 changes: 2 additions & 0 deletions yarn-project/simulator/src/public/public_execution_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ export class PublicExecutionContext extends TypedOracle {
isStaticCall: boolean,
isDelegateCall: boolean,
) {
isStaticCall = isStaticCall || this.execution.callContext.isStaticCall;

const args = this.packedArgsCache.unpack(argsHash);
this.log(`Public function call: addr=${targetContractAddress} selector=${functionSelector} args=${args.join(',')}`);

Expand Down
9 changes: 2 additions & 7 deletions yellow-paper/docs/calls/static-calls.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,6 @@
In particular, the following fields of the returned `CallStackItem` must be zero or empty in a static call:

<!-- Please can we have a similar list for the side effects of a public call? We're missing things like public state writes. -->
<!--
What about nested calls? Is a function which is 'staticcalled' allowed to make calls?
I think the options are:
- No. Or,
- Yes, but any nested calls from a staticcalled function must also be static calls.
Thoughts? Ethereum does the latter. We should write about whichever we choose in this page.
-->

- `new_note_hashes`
- `new_nullifiers`
Expand All @@ -22,6 +15,8 @@ Thoughts? Ethereum does the latter. We should write about whichever we choose in
- `encrypted_log_preimages_length`
- `unencrypted_log_preimages_length`

From the moment a static call is made, every subsequent nested call is forced to be static by setting a flag in the derived `CallContext`, which propagates through the call stack.

At the protocol level, a static call is identified by a `is_static_call` flag in the `CircuitPublicInputs` of the `CallStackItem`. The kernel is responsible for asserting that the call and all nested calls do not emit any forbidden side effects.

At the contract level, a caller can initiate a static call via a `staticCallPrivateFunction` or `staticCallPublicFunction` oracle call. The caller is responsible for asserting that the returned `CallStackItem` has the `is_static_call` flag correctly set.

0 comments on commit bd5c879

Please sign in to comment.