Skip to content

Commit 62963f9

Browse files
authored
feat!: refactor contract interaction pt.1 (#8938)
Part 1 of a refactor to `ContractFunctionInteraction` ## PXE APIs better reflect the lifecycle of a TX: `execute private -> simulate kernels -> simulate public (estimate gas) -> prove -> send` `prove` potentially goes back to `execute private` (so we can swap to a `FakeAccountContract` for example), but that's a story for another time. ## Removal of weird `SimulatedTx` class, now we have: - `PrivateExecutionResult` (old `ExecutionResult`) - `PrivateSimulationResult`: output of private simulation + kernel simulation: includes the former and `PrivateKernelTailCircuitPublicInputs` - `TxSimulationResult`: output of private + kernel + public simulation: includes the former + public outputs - `TxProvingResult`: output of private + proving: includes everything in `PrivateSimulationResult` + clientIvcProof ## Removal of mutability in `ContractFunctionInteraction` no more `this.txRequest = undefined` nonsense Now you can `.prove()` a `BaseContractInteraction`, which returns a `ProvenTx`. This doesn't mutate the original object, and the returned value can later be `.send()`. ```typescript // Example from block_building e2e, used to generate a block with multiple txs in it: const provenTxs = []; for (let i = 0; i < TX_COUNT; i++) { provenTxs.push( await methods[i].prove({ contractAddressSalt: new Fr(BigInt(i + 1)), skipClassRegistration: true, skipPublicDeployment: true, }), ); } // Send them simultaneously to be picked up by the sequencer const txs = await Promise.all(provenTxs.map(tx => tx.send())); logger.info(`Txs sent with hashes: `); for (const tx of txs) { logger.info(` ${await tx.getTxHash()}`); } ```
1 parent 9c21fc7 commit 62963f9

File tree

73 files changed

+1282
-859
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+1282
-859
lines changed

docs/docs/migration_notes.md

+23
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,29 @@ Aztec is in full-speed development. Literally every version breaks compatibility
88

99
## TBD
1010

11+
### Changes to PXE API and `ContractFunctionInteraction``
12+
13+
PXE APIs have been refactored to better reflext the lifecycle of a Tx (`execute private -> simulate kernels -> simulate public (estimate gas) -> prove -> send`)
14+
15+
* `.simulateTx`: Now returns a `TxSimulationResult`, containing the output of private execution, kernel simulation and public simulation (optional).
16+
* `.proveTx`: Now accepts the result of executing the private part of a transaction, so simulation doesn't have to happen again.
17+
18+
Thanks to this refactor, `ContractFunctionInteraction` has been updated to remove its internal cache and avoid bugs due to its mutable nature. As a result our type-safe interfaces now have to be used as follows:
19+
20+
```diff
21+
-const action = MyContract.at(address).method(args);
22+
-await action.prove();
23+
-await action.send().wait();
24+
+const action = MyContract.at(address).method(args);
25+
+const provenTx = await action.prove();
26+
+await provenTx.send().wait();
27+
```
28+
29+
It's still possible to use `.send()` as before, which will perform proving under the hood.
30+
31+
More changes are coming to these APIs to better support gas estimation mechanisms and advanced features.
32+
33+
1134
### Changes to public calling convention
1235

1336
Contracts that include public functions (that is, marked with `#[public]`), are required to have a function `public_dispatch(selector: Field)` which acts as an entry point. This will be soon the only public function registered/deployed in contracts. The calling convention is updated so that external calls are made to this function.

yarn-project/accounts/src/testing/configuration.ts

+15-13
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { generatePublicKey } from '@aztec/aztec.js';
2-
import { type AccountWalletWithSecretKey } from '@aztec/aztec.js/wallet';
2+
import { registerContractClass } from '@aztec/aztec.js/deployment';
3+
import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint';
4+
import { type AccountWalletWithSecretKey, SignerlessWallet } from '@aztec/aztec.js/wallet';
35
import { type PXE } from '@aztec/circuit-types';
46
import { deriveMasterIncomingViewingSecretKey, deriveSigningKey } from '@aztec/circuits.js/keys';
57
import { Fr } from '@aztec/foundation/fields';
68

7-
import { getSchnorrAccount } from '../schnorr/index.js';
9+
import { SchnorrAccountContractArtifact, getSchnorrAccount } from '../schnorr/index.js';
810

911
export const INITIAL_TEST_SECRET_KEYS = [
1012
Fr.fromString('2153536ff6628eee01cf4024889ff977a18d9fa61d0e414422f7681cf085c281'),
@@ -69,28 +71,28 @@ export async function deployInitialTestAccounts(pxe: PXE) {
6971
secretKey,
7072
};
7173
});
74+
// Register contract class to avoid duplicate nullifier errors
75+
const { l1ChainId: chainId, protocolVersion } = await pxe.getNodeInfo();
76+
const deployWallet = new SignerlessWallet(pxe, new DefaultMultiCallEntrypoint(chainId, protocolVersion));
77+
await (await registerContractClass(deployWallet, SchnorrAccountContractArtifact)).send().wait();
7278
// Attempt to get as much parallelism as possible
73-
const deployMethods = await Promise.all(
79+
const deployTxs = await Promise.all(
7480
accounts.map(async x => {
7581
const deployMethod = await x.account.getDeployMethod();
76-
await deployMethod.create({
82+
const tx = await deployMethod.prove({
7783
contractAddressSalt: x.account.salt,
78-
skipClassRegistration: true,
79-
skipPublicDeployment: true,
8084
universalDeploy: true,
8185
});
82-
await deployMethod.prove({});
83-
return deployMethod;
86+
return tx;
8487
}),
8588
);
8689
// Send tx together to try and get them in the same rollup
87-
const sentTxs = deployMethods.map(dm => {
88-
return dm.send();
90+
const sentTxs = deployTxs.map(tx => {
91+
return tx.send();
8992
});
9093
await Promise.all(
91-
sentTxs.map(async (tx, i) => {
92-
const wallet = await accounts[i].account.getWallet();
93-
return tx.wait({ wallet });
94+
sentTxs.map(tx => {
95+
return tx.wait();
9496
}),
9597
);
9698
return accounts;

yarn-project/accounts/src/testing/create_account.ts

+12-17
Original file line numberDiff line numberDiff line change
@@ -30,34 +30,29 @@ export async function createAccounts(
3030
secrets: Fr[] = [],
3131
waitOpts: WaitOpts = { interval: 0.1 },
3232
): Promise<AccountWalletWithSecretKey[]> {
33-
const accounts = [];
34-
3533
if (secrets.length == 0) {
3634
secrets = Array.from({ length: numberOfAccounts }, () => Fr.random());
3735
} else if (secrets.length > 0 && secrets.length !== numberOfAccounts) {
3836
throw new Error('Secrets array must be empty or have the same length as the number of accounts');
3937
}
4038

4139
// Prepare deployments
42-
for (const secret of secrets) {
43-
const signingKey = deriveSigningKey(secret);
44-
const account = getSchnorrAccount(pxe, secret, signingKey);
45-
// Unfortunately the function below is not stateless and we call it here because it takes a long time to run and
46-
// the results get stored within the account object. By calling it here we increase the probability of all the
47-
// accounts being deployed in the same block because it makes the deploy() method basically instant.
48-
await account.getDeployMethod().then(d =>
49-
d.prove({
40+
const accountsAndDeployments = await Promise.all(
41+
secrets.map(async secret => {
42+
const signingKey = deriveSigningKey(secret);
43+
const account = getSchnorrAccount(pxe, secret, signingKey);
44+
const deployMethod = await account.getDeployMethod();
45+
const provenTx = await deployMethod.prove({
5046
contractAddressSalt: account.salt,
5147
skipClassRegistration: true,
5248
skipPublicDeployment: true,
5349
universalDeploy: true,
54-
}),
55-
);
56-
accounts.push(account);
57-
}
50+
});
51+
return { account, provenTx };
52+
}),
53+
);
5854

5955
// Send them and await them to be mined
60-
const txs = await Promise.all(accounts.map(account => account.deploy()));
61-
await Promise.all(txs.map(tx => tx.wait(waitOpts)));
62-
return Promise.all(accounts.map(account => account.getWallet()));
56+
await Promise.all(accountsAndDeployments.map(({ provenTx }) => provenTx.send().wait(waitOpts)));
57+
return Promise.all(accountsAndDeployments.map(({ account }) => account.getWallet()));
6358
}

yarn-project/aztec.js/src/account_manager/deploy_account_sent_tx.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type PXE, type TxHash, type TxReceipt } from '@aztec/circuit-types';
22
import { type FieldsOf } from '@aztec/foundation/types';
33

44
import { type Wallet } from '../account/index.js';
5-
import { DefaultWaitOpts, SentTx, type WaitOpts } from '../contract/index.js';
5+
import { DefaultWaitOpts, SentTx, type WaitOpts } from '../contract/sent_tx.js';
66
import { waitForAccountSynch } from '../utils/account.js';
77

88
/** Extends a transaction receipt with a wallet instance for the newly deployed contract. */

yarn-project/aztec.js/src/account_manager/index.ts

+22-26
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export class AccountManager {
3434
private completeAddress?: CompleteAddress;
3535
private instance?: ContractInstanceWithAddress;
3636
private publicKeysHash?: Fr;
37-
private deployMethod?: DeployAccountMethod;
3837

3938
constructor(private pxe: PXE, private secretKey: Fr, private accountContract: AccountContract, salt?: Salt) {
4039
this.salt = salt !== undefined ? new Fr(salt) : Fr.random();
@@ -131,33 +130,30 @@ export class AccountManager {
131130
* @returns A DeployMethod instance that deploys this account contract.
132131
*/
133132
public async getDeployMethod() {
134-
if (!this.deployMethod) {
135-
if (!this.isDeployable()) {
136-
throw new Error(
137-
`Account contract ${this.accountContract.getContractArtifact().name} does not require deployment.`,
138-
);
139-
}
140-
141-
await this.pxe.registerAccount(this.secretKey, this.getCompleteAddress().partialAddress);
142-
143-
const { l1ChainId: chainId, protocolVersion } = await this.pxe.getNodeInfo();
144-
const deployWallet = new SignerlessWallet(this.pxe, new DefaultMultiCallEntrypoint(chainId, protocolVersion));
145-
146-
// We use a signerless wallet with the multi call entrypoint in order to make multiple calls in one go
147-
// If we used getWallet, the deployment would get routed via the account contract entrypoint
148-
// and it can't be used unless the contract is initialized
149-
const args = this.accountContract.getDeploymentArgs() ?? [];
150-
this.deployMethod = new DeployAccountMethod(
151-
this.accountContract.getAuthWitnessProvider(this.getCompleteAddress()),
152-
this.getPublicKeysHash(),
153-
deployWallet,
154-
this.accountContract.getContractArtifact(),
155-
args,
156-
'constructor',
157-
'entrypoint',
133+
if (!this.isDeployable()) {
134+
throw new Error(
135+
`Account contract ${this.accountContract.getContractArtifact().name} does not require deployment.`,
158136
);
159137
}
160-
return this.deployMethod;
138+
139+
await this.pxe.registerAccount(this.secretKey, this.getCompleteAddress().partialAddress);
140+
141+
const { l1ChainId: chainId, protocolVersion } = await this.pxe.getNodeInfo();
142+
const deployWallet = new SignerlessWallet(this.pxe, new DefaultMultiCallEntrypoint(chainId, protocolVersion));
143+
144+
// We use a signerless wallet with the multi call entrypoint in order to make multiple calls in one go
145+
// If we used getWallet, the deployment would get routed via the account contract entrypoint
146+
// and it can't be used unless the contract is initialized
147+
const args = this.accountContract.getDeploymentArgs() ?? [];
148+
return new DeployAccountMethod(
149+
this.accountContract.getAuthWitnessProvider(this.getCompleteAddress()),
150+
this.getPublicKeysHash(),
151+
deployWallet,
152+
this.accountContract.getContractArtifact(),
153+
args,
154+
'constructor',
155+
'entrypoint',
156+
);
161157
}
162158

163159
/**

yarn-project/aztec.js/src/contract/base_contract_interaction.ts

+21-24
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { type Tx, type TxExecutionRequest } from '@aztec/circuit-types';
1+
import { type TxExecutionRequest, type TxProvingResult } from '@aztec/circuit-types';
22
import { type Fr, GasSettings } from '@aztec/circuits.js';
33
import { createDebugLogger } from '@aztec/foundation/log';
44

55
import { type Wallet } from '../account/wallet.js';
66
import { type ExecutionRequestInit, type FeeOptions } from '../entrypoint/entrypoint.js';
77
import { getGasLimits } from './get_gas_limits.js';
8+
import { ProvenTx } from './proven_tx.js';
89
import { SentTx } from './sent_tx.js';
910

1011
/**
@@ -29,13 +30,6 @@ export type SendMethodOptions = {
2930
* Implements the sequence create/simulate/send.
3031
*/
3132
export abstract class BaseContractInteraction {
32-
/**
33-
* The transaction execution result. Set by prove().
34-
* Made public for simple mocking.
35-
*/
36-
public tx?: Tx;
37-
protected txRequest?: TxExecutionRequest;
38-
3933
protected log = createDebugLogger('aztec:js:contract_interaction');
4034

4135
constructor(protected wallet: Wallet) {}
@@ -47,15 +41,27 @@ export abstract class BaseContractInteraction {
4741
*/
4842
public abstract create(options?: SendMethodOptions): Promise<TxExecutionRequest>;
4943

44+
/**
45+
* Creates a transaction execution request, simulates and proves it. Differs from .prove in
46+
* that its result does not include the wallet nor the composed tx object, but only the proving result.
47+
* This object can then be used to either create a ProvenTx ready to be sent, or directly send the transaction.
48+
* @param options - optional arguments to be used in the creation of the transaction
49+
* @returns The proving result.
50+
*/
51+
protected async proveInternal(options: SendMethodOptions = {}): Promise<TxProvingResult> {
52+
const txRequest = await this.create(options);
53+
const txSimulationResult = await this.wallet.simulateTx(txRequest, !options.skipPublicSimulation, undefined, true);
54+
return await this.wallet.proveTx(txRequest, txSimulationResult.privateExecutionResult);
55+
}
56+
5057
/**
5158
* Proves a transaction execution request and returns a tx object ready to be sent.
5259
* @param options - optional arguments to be used in the creation of the transaction
5360
* @returns The resulting transaction
5461
*/
55-
public async prove(options: SendMethodOptions = {}): Promise<Tx> {
56-
const txRequest = this.txRequest ?? (await this.create(options));
57-
this.tx = await this.wallet.proveTx(txRequest, !options.skipPublicSimulation);
58-
return this.tx;
62+
public async prove(options: SendMethodOptions = {}): Promise<ProvenTx> {
63+
const txProvingResult = await this.proveInternal(options);
64+
return new ProvenTx(this.wallet, txProvingResult.toTx());
5965
}
6066

6167
/**
@@ -67,12 +73,11 @@ export abstract class BaseContractInteraction {
6773
* the AztecAddress of the sender. If not provided, the default address is used.
6874
* @returns A SentTx instance for tracking the transaction status and information.
6975
*/
70-
public send(options: SendMethodOptions = {}) {
76+
public send(options: SendMethodOptions = {}): SentTx {
7177
const promise = (async () => {
72-
const tx = this.tx ?? (await this.prove(options));
73-
return this.wallet.sendTx(tx);
78+
const txProvingResult = await this.proveInternal(options);
79+
return this.wallet.sendTx(txProvingResult.toTx());
7480
})();
75-
7681
return new SentTx(this.wallet, promise);
7782
}
7883

@@ -84,15 +89,7 @@ export abstract class BaseContractInteraction {
8489
public async estimateGas(
8590
opts?: Omit<SendMethodOptions, 'estimateGas' | 'skipPublicSimulation'>,
8691
): Promise<Pick<GasSettings, 'gasLimits' | 'teardownGasLimits'>> {
87-
// REFACTOR: both `this.txRequest = undefined` below are horrible, we should not be caching stuff that doesn't need to be.
88-
// This also hints at a weird interface for create/request/estimate/send etc.
89-
90-
// Ensure we don't accidentally use a version of tx request that has estimateGas set to true, leading to an infinite loop.
91-
this.txRequest = undefined;
9292
const txRequest = await this.create({ ...opts, estimateGas: false });
93-
// Ensure we don't accidentally cache a version of tx request that has estimateGas forcefully set to false.
94-
this.txRequest = undefined;
95-
9693
const simulationResult = await this.wallet.simulateTx(txRequest, true);
9794
const { totalGas: gasLimits, teardownGas: teardownGasLimits } = getGasLimits(
9895
simulationResult,

yarn-project/aztec.js/src/contract/batch_call.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,9 @@ export class BatchCall extends BaseContractInteraction {
1818
* @returns A Promise that resolves to a transaction instance.
1919
*/
2020
public async create(opts?: SendMethodOptions): Promise<TxExecutionRequest> {
21-
if (!this.txRequest) {
22-
const calls = this.calls;
23-
const fee = opts?.estimateGas ? await this.getFeeOptionsFromEstimatedGas({ calls, fee: opts?.fee }) : opts?.fee;
24-
this.txRequest = await this.wallet.createTxExecutionRequest({ calls, fee });
25-
}
26-
return this.txRequest;
21+
const calls = this.calls;
22+
const fee = opts?.estimateGas ? await this.getFeeOptionsFromEstimatedGas({ calls, fee: opts?.fee }) : opts?.fee;
23+
return await this.wallet.createTxExecutionRequest({ calls, fee });
2724
}
2825

2926
/**
@@ -92,8 +89,8 @@ export class BatchCall extends BaseContractInteraction {
9289
// For public functions we retrieve the first values directly from the public output.
9390
const rawReturnValues =
9491
call.type == FunctionType.PRIVATE
95-
? simulatedTx.privateReturnValues?.nested?.[resultIndex].values
96-
: simulatedTx.publicOutput?.publicReturnValues?.[resultIndex].values;
92+
? simulatedTx.getPrivateReturnValues()?.nested?.[resultIndex].values
93+
: simulatedTx.getPublicReturnValues()?.[resultIndex].values;
9794

9895
results[callIndex] = rawReturnValues ? decodeFromAbi(call.returnTypes, rawReturnValues) : [];
9996
});

yarn-project/aztec.js/src/contract/contract.test.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1-
import { type Tx, type TxExecutionRequest, type TxHash, type TxReceipt } from '@aztec/circuit-types';
1+
import {
2+
type Tx,
3+
type TxExecutionRequest,
4+
type TxHash,
5+
type TxProvingResult,
6+
type TxReceipt,
7+
type TxSimulationResult,
8+
} from '@aztec/circuit-types';
29
import { AztecAddress, CompleteAddress, EthAddress } from '@aztec/circuits.js';
310
import { type L1ContractAddresses } from '@aztec/ethereum';
411
import { type AbiDecoded, type ContractArtifact, FunctionType } from '@aztec/foundation/abi';
12+
import { type ContractInstanceWithAddress } from '@aztec/types/contracts';
513
import { type NodeInfo } from '@aztec/types/interfaces';
614

715
import { type MockProxy, mock } from 'jest-mock-extended';
816

9-
import { type ContractInstanceWithAddress } from '../index.js';
10-
import { type Wallet } from '../wallet/index.js';
17+
import { type Wallet } from '../account/wallet.js';
1118
import { Contract } from './contract.js';
1219

1320
describe('Contract Class', () => {
@@ -17,9 +24,11 @@ describe('Contract Class', () => {
1724
let contractInstance: ContractInstanceWithAddress;
1825

1926
const mockTx = { type: 'Tx' } as any as Tx;
27+
const mockTxProvingResult = { type: 'TxProvingResult', toTx: () => mockTx } as any as TxProvingResult;
2028
const mockTxRequest = { type: 'TxRequest' } as any as TxExecutionRequest;
2129
const mockTxHash = { type: 'TxHash' } as any as TxHash;
2230
const mockTxReceipt = { type: 'TxReceipt' } as any as TxReceipt;
31+
const mockTxSimulationResult = { type: 'TxSimulationResult' } as any as TxSimulationResult;
2332
const mockUnconstrainedResultValue = 1;
2433
const l1Addresses: L1ContractAddresses = {
2534
rollupAddress: EthAddress.random(),
@@ -124,13 +133,14 @@ describe('Contract Class', () => {
124133
contractInstance = { address: contractAddress } as ContractInstanceWithAddress;
125134

126135
wallet = mock<Wallet>();
136+
wallet.simulateTx.mockResolvedValue(mockTxSimulationResult);
127137
wallet.createTxExecutionRequest.mockResolvedValue(mockTxRequest);
128138
wallet.getContractInstance.mockResolvedValue(contractInstance);
129139
wallet.sendTx.mockResolvedValue(mockTxHash);
130140
wallet.simulateUnconstrained.mockResolvedValue(mockUnconstrainedResultValue as any as AbiDecoded);
131141
wallet.getTxReceipt.mockResolvedValue(mockTxReceipt);
132142
wallet.getNodeInfo.mockResolvedValue(mockNodeInfo);
133-
wallet.proveTx.mockResolvedValue(mockTx);
143+
wallet.proveTx.mockResolvedValue(mockTxProvingResult);
134144
wallet.getRegisteredAccounts.mockResolvedValue([account]);
135145
});
136146

0 commit comments

Comments
 (0)