Skip to content

Commit

Permalink
feat(SimulateTx): simulate constrained transaction execution with ret…
Browse files Browse the repository at this point in the history
…urn values (#5432)

This PR replaces the old `view` function with a `simulate` that can be
executed on `private`, `public` and `unconstrained` functions.

It have a limitation in the format of the return values. Currently it is
returning a size 4 array of Fields, as that is the return value that the
artifact knows about.

It is to be fixed as part of #5450 that will get the proper values into
the artifacts such that we can decode it nicely.
  • Loading branch information
LHerskind authored Apr 3, 2024
1 parent eb3acdf commit 0249737
Show file tree
Hide file tree
Showing 82 changed files with 868 additions and 552 deletions.
2 changes: 1 addition & 1 deletion boxes/boxes/react/src/hooks/useNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function useNumber({ contract }: { contract: Contract }) {

setWait(true);
const deployerWallet = await deployerEnv.getWallet();
const viewTxReceipt = await contract!.methods.getNumber(deployerWallet.getCompleteAddress()).view();
const viewTxReceipt = await contract!.methods.getNumber(deployerWallet.getCompleteAddress()).simulate();
toast(`Number is: ${viewTxReceipt.value}`);
setWait(false);
};
Expand Down
2 changes: 1 addition & 1 deletion boxes/boxes/react/tests/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('BoxReact Contract Tests', () => {
}, 40000);

test('Can read a number', async () => {
const viewTxReceipt = await contract.methods.getNumber(wallet.getCompleteAddress()).view();
const viewTxReceipt = await contract.methods.getNumber(wallet.getCompleteAddress()).simulate();
expect(numberToSet.toBigInt()).toEqual(viewTxReceipt.value);
}, 40000);
});
2 changes: 1 addition & 1 deletion boxes/boxes/vanilla/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ document.querySelector('#set').addEventListener('submit', async (e: Event) => {
});

document.querySelector('#get').addEventListener('click', async () => {
const viewTxReceipt = await contract.methods.getNumber(wallet.getCompleteAddress().address).view();
const viewTxReceipt = await contract.methods.getNumber(wallet.getCompleteAddress().address).simulate();
alert(`Number is: ${viewTxReceipt.value}`);
});
14 changes: 9 additions & 5 deletions docs/docs/developers/aztecjs/guides/call_view_function.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: How to Call a View Function
title: How to Simulate a Function Call
---

This guide explains how to call a `view` function using [Aztec.js](../main.md).
This guide explains how to `simulate` a function call using [Aztec.js](../main.md).

To do this from the CLI, go [here](../../sandbox/references/cli-commands.md#calling-an-unconstrained-view-function).

Expand All @@ -26,9 +26,13 @@ Get a previously deployed contract like this:

#include_code get_contract yarn-project/end-to-end/src/docs_examples.test.ts typescript

## Call view function
## Simulating function calls

Call the `view` function on the contract like this:
Call the `simulate` function on the typescript contract wrapper like this:

#include_code call_view_function yarn-project/end-to-end/src/docs_examples.test.ts typescript
#include_code simulate_function yarn-project/end-to-end/src/docs_examples.test.ts typescript

:::info Note
- If the simulated function is `unconstrained` you will get a properly typed value.
- If the simulated function is `public` or `private` it will return a Field array of size 4.
:::
2 changes: 1 addition & 1 deletion docs/docs/developers/tutorials/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ WARN Error processing tx 06dc87c4d64462916ea58426ffcfaf20017880b353c9ec3e0f0ee5f

### State

We can check private or public state directly rather than going through view-only methods, as we did in the initial example by calling `token.methods.balance().view()`. Bear in mind that directly accessing contract storage will break any kind of encapsulation.
We can check private or public state directly rather than going through view-only methods, as we did in the initial example by calling `token.methods.balance().simulate()`. Bear in mind that directly accessing contract storage will break any kind of encapsulation.

To query storage directly, you'll need to know the slot you want to access. This can be checked in the [contract's `Storage` definition](../contracts/writing_contracts/storage/main.md) directly for most data types. However, when it comes to mapping types, as in most EVM languages, we'll need to calculate the slot for a given key. To do this, we'll use the [`CheatCodes`](../sandbox/references/cheat_codes.md) utility class:

Expand Down
30 changes: 28 additions & 2 deletions docs/docs/misc/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,32 @@ This change was made to communicate that we do not constrain the value in circui
+ let random_value = unsafe_rand();
```

### [AztecJS] Simulate and get return values for ANY call
Historically it have been possible to "view" `unconstrained` functions to simulate them and get the return values, but not for `public` nor `private` functions.
This has lead to a lot of bad code where we have the same function implemented thrice, once in `private`, once in `public` and once in `unconstrained`.
It is not possible to call `simulate` on any call to get the return values!
However, beware that it currently always returns a Field array of size 4 for private and public.
This will change to become similar to the return values of the `unconstrained` functions with proper return types.

```diff
- #[aztec(private)]
- fn get_shared_immutable_constrained_private() -> pub Leader {
- storage.shared_immutable.read_private()
- }
-
- unconstrained fn get_shared_immutable() -> pub Leader {
- storage.shared_immutable.read_public()
- }

+ #[aztec(private)]
+ fn get_shared_immutable_private() -> pub Leader {
+ storage.shared_immutable.read_private()
+ }

- const returnValues = await contract.methods.get_shared_immutable().view();
+ const returnValues = await contract.methods.get_shared_immutable_private().simulate();
```

## 0.31.0

### [Aztec.nr] Public storage historical read API improvement
Expand Down Expand Up @@ -935,13 +961,13 @@ To parse a `AztecAddress` to BigInt, use `.inner`
Before:

```js
const tokenBigInt = await bridge.methods.token().view();
const tokenBigInt = await bridge.methods.token().simulate();
```

Now:

```js
const tokenBigInt = (await bridge.methods.token().view()).inner;
const tokenBigInt = (await bridge.methods.token().simulate()).inner;
```

### [Aztec.nr] Add `protocol_types` to Nargo.toml
Expand Down
1 change: 0 additions & 1 deletion docs/internal_notes/building_dapps.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ Explain how to write a dapp using [`aztec.js`](https://github.com/AztecProtocol/
- Instantiate a contract
- Deploy a contract
- How to generate a nice typescript interface for an Aztec.nr contract's functions (we have a little `.ts` program in `noir-contracts` to generate a types file at the moment... how would a user do this?)
- Call 'view' functions
- Simulate functions (simulate the result, without sending to the 'network')
- Execute functions (send them to the 'network')
- Tx hashes and tx receipts
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,42 @@ contract DocsExample {
assert(read.points == expected.points, "Invalid points");
}

#[aztec(private)]
fn get_shared_immutable_constrained_private_indirect() -> pub Leader {
// This is a private function that calls another private function
// and returns the response.
// Used to test that we can retrieve values through calls and
// correctly return them in the simulation
let ret = context.call_private_function_no_args(
context.this_address(),
FunctionSelector::from_signature("get_shared_immutable_constrained_private()")
);
Leader::deserialize([ret[0], ret[1]])
}

#[aztec(public)]
fn get_shared_immutable_constrained_public_indirect() -> pub Leader {
// This is a public function that calls another public function
// and returns the response.
// Used to test that we can retrieve values through calls and
// correctly return them in the simulation
let ret = context.call_public_function_no_args(
context.this_address(),
FunctionSelector::from_signature("get_shared_immutable_constrained_public()")
);
Leader::deserialize([ret[0], ret[1]])
}

#[aztec(public)]
fn get_shared_immutable_constrained_public() -> pub Leader {
storage.shared_immutable.read_public()
}

#[aztec(private)]
fn get_shared_immutable_constrained_private() -> pub Leader {
storage.shared_immutable.read_private()
}

unconstrained fn get_shared_immutable() -> pub Leader {
storage.shared_immutable.read_public()
}
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/accounts/src/testing/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export async function deployInitialTestAccounts(pxe: PXE) {
skipPublicDeployment: true,
universalDeploy: true,
});
await deployMethod.simulate({});
await deployMethod.prove({});
return deployMethod;
}),
);
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/accounts/src/testing/create_account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function createAccounts(pxe: PXE, numberOfAccounts = 1): Promise<Ac
// the results get stored within the account object. By calling it here we increase the probability of all the
// accounts being deployed in the same block because it makes the deploy() method basically instant.
await account.getDeployMethod().then(d =>
d.simulate({
d.prove({
contractAddressSalt: account.salt,
skipClassRegistration: true,
skipPublicDeployment: true,
Expand Down
3 changes: 2 additions & 1 deletion yarn-project/aztec-node/src/aztec-node/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ export class AztecNodeService implements AztecNode {
new WASMSimulator(),
);
const processor = await publicProcessorFactory.create(prevHeader, newGlobalVariables);
const [processedTxs, failedTxs] = await processor.process([tx]);
const [processedTxs, failedTxs, returns] = await processor.process([tx]);
if (failedTxs.length) {
this.log.warn(`Simulated tx ${tx.getTxHash()} fails: ${failedTxs[0].error}`);
throw failedTxs[0].error;
Expand All @@ -676,6 +676,7 @@ export class AztecNodeService implements AztecNode {
throw reverted[0].revertReason;
}
this.log.info(`Simulated tx ${tx.getTxHash()} succeeds`);
return returns;
}

public setConfig(config: Partial<SequencerConfig>): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/aztec.js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ const tx = await contract.methods.transfer(amount, recipientAddress).send().wait
console.log(`Transferred ${amount} to ${recipientAddress} on block ${tx.blockNumber}`);
```

### Call a view function
### Simulate a function

```typescript
import { Contract } from '@aztec/aztec.js';

const contract = await Contract.at(contractAddress, MyContractArtifact, wallet);
const balance = await contract.methods.get_balance(wallet.getAddress()).view();
const balance = await contract.methods.get_balance(wallet.getAddress()).simulate();
console.log(`Account balance is ${balance}`);
```
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ export abstract class BaseContractInteraction {
public abstract create(options?: SendMethodOptions): Promise<TxExecutionRequest>;

/**
* Simulates a transaction execution request and returns a tx object ready to be sent.
* Proves a transaction execution request and returns a tx object ready to be sent.
* @param options - optional arguments to be used in the creation of the transaction
* @returns The resulting transaction
*/
public async simulate(options: SendMethodOptions = {}): Promise<Tx> {
public async prove(options: SendMethodOptions = {}): Promise<Tx> {
const txRequest = this.txRequest ?? (await this.create(options));
this.tx = await this.pxe.simulateTx(txRequest, !options.skipPublicSimulation);
this.tx = await this.pxe.proveTx(txRequest, !options.skipPublicSimulation);
return this.tx;
}

Expand All @@ -58,7 +58,7 @@ export abstract class BaseContractInteraction {
*/
public send(options: SendMethodOptions = {}) {
const promise = (async () => {
const tx = this.tx ?? (await this.simulate(options));
const tx = this.tx ?? (await this.prove(options));
return this.pxe.sendTx(tx);
})();

Expand Down
14 changes: 4 additions & 10 deletions yarn-project/aztec.js/src/contract/contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type Tx, type TxExecutionRequest, type TxHash, type TxReceipt } from '@aztec/circuit-types';
import { AztecAddress, CompleteAddress, EthAddress } from '@aztec/circuits.js';
import { type L1ContractAddresses } from '@aztec/ethereum';
import { ABIParameterVisibility, type ContractArtifact, FunctionType } from '@aztec/foundation/abi';
import { ABIParameterVisibility, type ContractArtifact, type DecodedReturn, FunctionType } from '@aztec/foundation/abi';
import { type NodeInfo } from '@aztec/types/interfaces';

import { type MockProxy, mock } from 'jest-mock-extended';
Expand Down Expand Up @@ -113,10 +113,10 @@ describe('Contract Class', () => {
wallet.createTxExecutionRequest.mockResolvedValue(mockTxRequest);
wallet.getContractInstance.mockResolvedValue(contractInstance);
wallet.sendTx.mockResolvedValue(mockTxHash);
wallet.viewTx.mockResolvedValue(mockViewResultValue);
wallet.viewTx.mockResolvedValue(mockViewResultValue as any as DecodedReturn);
wallet.getTxReceipt.mockResolvedValue(mockTxReceipt);
wallet.getNodeInfo.mockResolvedValue(mockNodeInfo);
wallet.simulateTx.mockResolvedValue(mockTx);
wallet.proveTx.mockResolvedValue(mockTx);
wallet.getRegisteredAccounts.mockResolvedValue([account]);
});

Expand All @@ -137,7 +137,7 @@ describe('Contract Class', () => {

it('should call view on an unconstrained function', async () => {
const fooContract = await Contract.at(contractAddress, defaultArtifact, wallet);
const result = await fooContract.methods.qux(123n).view({
const result = await fooContract.methods.qux(123n).simulate({
from: account.address,
});
expect(wallet.viewTx).toHaveBeenCalledTimes(1);
Expand All @@ -149,10 +149,4 @@ describe('Contract Class', () => {
const fooContract = await Contract.at(contractAddress, defaultArtifact, wallet);
await expect(fooContract.methods.qux().create()).rejects.toThrow();
});

it('should not call view on a secret or open function', async () => {
const fooContract = await Contract.at(contractAddress, defaultArtifact, wallet);
expect(() => fooContract.methods.bar().view()).toThrow();
expect(() => fooContract.methods.baz().view()).toThrow();
});
});
57 changes: 44 additions & 13 deletions yarn-project/aztec.js/src/contract/contract_function_interaction.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type FunctionCall, type TxExecutionRequest } from '@aztec/circuit-types';
import { type AztecAddress, FunctionData } from '@aztec/circuits.js';
import { type FunctionCall, PackedArguments, TxExecutionRequest } from '@aztec/circuit-types';
import { type AztecAddress, FunctionData, TxContext } from '@aztec/circuits.js';
import { type FunctionAbi, FunctionType, encodeArguments } from '@aztec/foundation/abi';

import { type Wallet } from '../account/wallet.js';
Expand All @@ -8,10 +8,11 @@ import { BaseContractInteraction, SendMethodOptions } from './base_contract_inte
export { SendMethodOptions };

/**
* Represents the options for a view method in a contract function interaction.
* Represents the options for simulating a contract function interaction.
* Allows specifying the address from which the view method should be called.
* Disregarded for simulation of public functions
*/
export type ViewMethodOptions = {
export type SimulateMethodOptions = {
/**
* The sender's Aztec address.
*/
Expand Down Expand Up @@ -63,18 +64,48 @@ export class ContractFunctionInteraction extends BaseContractInteraction {
}

/**
* Execute a view (read-only) transaction on an unconstrained function.
* This method is used to call functions that do not modify the contract state and only return data.
* Throws an error if called on a non-unconstrained function.
* Simulate a transaction and get its return values
* Differs from prove in a few important ways:
* 1. It returns the values of the function execution
* 2. It supports `unconstrained`, `private` and `public` functions
* 3. For `private` execution it:
* 3.a SKIPS the entrypoint and starts directly at the function
* 3.b SKIPS public execution entirely
* 4. For `public` execution it:
* 4.a Removes the `txRequest` value after ended simulation
* 4.b Ignores the `from` in the options
*
* @param options - An optional object containing additional configuration for the transaction.
* @returns The result of the view transaction as returned by the contract function.
* @returns The result of the transaction as returned by the contract function.
*/
public view(options: ViewMethodOptions = {}) {
if (this.functionDao.functionType !== FunctionType.UNCONSTRAINED) {
throw new Error('Can only call `view` on an unconstrained function.');
public async simulate(options: SimulateMethodOptions = {}): Promise<any> {
if (this.functionDao.functionType == FunctionType.UNCONSTRAINED) {
return this.wallet.viewTx(this.functionDao.name, this.args, this.contractAddress, options.from);
}

const { from } = options;
return this.wallet.viewTx(this.functionDao.name, this.args, this.contractAddress, from);
// TODO: If not unconstrained, we return a size 4 array of fields.
// TODO: It should instead return the correctly decoded value
// TODO: The return type here needs to be fixed! @LHerskind

if (this.functionDao.functionType == FunctionType.SECRET) {
const nodeInfo = await this.wallet.getNodeInfo();
const packedArgs = PackedArguments.fromArgs(encodeArguments(this.functionDao, this.args));

const txRequest = TxExecutionRequest.from({
argsHash: packedArgs.hash,
origin: this.contractAddress,
functionData: FunctionData.fromAbi(this.functionDao),
txContext: TxContext.empty(nodeInfo.chainId, nodeInfo.protocolVersion),
packedArguments: [packedArgs],
authWitnesses: [],
});
const simulatedTx = await this.pxe.simulateTx(txRequest, false, options.from ?? this.wallet.getAddress());
return simulatedTx.privateReturnValues?.[0];
} else {
const txRequest = await this.create();
const simulatedTx = await this.pxe.simulateTx(txRequest, true);
this.txRequest = undefined;
return simulatedTx.publicReturnValues?.[0];
}
}
}
8 changes: 4 additions & 4 deletions yarn-project/aztec.js/src/contract/deploy_method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,12 @@ export class DeployMethod<TContract extends ContractBase = Contract> extends Bas
}

/**
* Simulate the request.
* Prove the request.
* @param options - Deployment options.
* @returns The simulated tx.
* @returns The proven tx.
*/
public simulate(options: DeployOptions): Promise<Tx> {
return super.simulate(options);
public prove(options: DeployOptions): Promise<Tx> {
return super.prove(options);
}

/** Return this deployment address. */
Expand Down
6 changes: 3 additions & 3 deletions yarn-project/aztec.js/src/contract/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
*
* The {@link Contract} class is the main class in this module, and provides static methods for deploying
* a contract or interacting with an already deployed one. The `methods` property of the contract instance
* provides access to private, public, and view methods, that can be invoked in a transaction via `send()`,
* or can be queried via `view()`.
* provides access to private, public, and simulate methods, that can be invoked in a transaction via `send()`,
* or can be queried via `simulate()`.
*
* ```ts
* const contract = await Contract.deploy(wallet, MyContractArtifact, [...constructorArgs]).send().deployed();
Expand All @@ -17,7 +17,7 @@
* ```ts
* const contract = await Contract.at(address, MyContractArtifact, wallet);
* await contract.methods.mint(1000, owner).send().wait();
* console.log(`Total supply is now ${await contract.methods.totalSupply().view()}`);
* console.log(`Total supply is now ${await contract.methods.totalSupply().simulate()}`);
* ```
*
* The result of calling a method in a contract instance, such as `contract.methods.mint(1000, owner)`
Expand Down
Loading

0 comments on commit 0249737

Please sign in to comment.