Skip to content

Commit 8486183

Browse files
nventuronchamo
andauthored
feat!: make contract classes dynamic (#24282)
This makes PXE stop trying to track a contract's current class. Instead, we fetch it on the fly from the node (which we already had to do anyway to make sure PXE's tracking of it was not out of sync). As a result, `pxe.updateContract` gets deleted. I introduced a contract class service, which deals with the interaction with the node for determining the current class (plus a cache to avoid repeated roundtrips), and an anchored contract data class for a given execution. There are some rough edges here (mostly in pxe.ts, which contains too much inlined code, and in some cases which receive both the anchored data and the raw store because the anchored data doesn't expose enough), but it's good enough for now I think. I created multiple follow up issues to clean some of this up. With this weird current class management gone, `registerContract` also became less important, so I simplified it to only take an instance, with `registerContractClass` taking the artifact. Both must be called. I also introduced a simpler version of `ContractInstance` (`ContractInstacePreimage`) which does not contain the current class - something only the AVM and the node care about - this is what PXE uses throughout. Once we fork the monorepo I imagine `ContractInstance` would be deleted on our side. Most other changes result from these decisions - the effects were quite far reaching. --------- Co-authored-by: Nicolas Chamo <nicolas@chamo.com.ar>
1 parent af64acc commit 8486183

54 files changed

Lines changed: 1155 additions & 746 deletions

File tree

Some content is hidden

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

docs/docs-developers/docs/aztec-js/how_to_test.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Use this to set up state preconditions, reproduce production bugs against pinned
8585

8686
### Fast-forwarding a contract update
8787

88-
`fastForwardContractUpdate` returns a `SimulationOverrides` object that simulates a deployed instance as if it had already been upgraded to a new contract class. The new class must already be registered on chain. The cheat mirrors a real `pxe.updateContract` followed by waiting out the upgrade delay: the instance's `currentContractClassId` is bumped, and the `ContractInstanceRegistry`'s delayed-public-mutable storage is rewritten to look like the upgrade was scheduled in the past.
88+
`fastForwardContractUpdate` returns a `SimulationOverrides` object that simulates a deployed instance as if it had already been upgraded to a new contract class. The new class must already be registered on chain. The cheat mirrors a real onchain upgrade followed by waiting out the upgrade delay: the override instance's `currentContractClassId` is bumped, and the `ContractInstanceRegistry`'s delayed-public-mutable storage is rewritten to look like the upgrade was scheduled in the past.
8989

9090
```typescript
9191
import { fastForwardContractUpdate } from '@aztec/aztec.js';

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,33 @@ Aztec is in active development. Each version may introduce breaking changes that
99

1010
## TBD
1111

12+
### [PXE] `pxe.updateContract` removed and `pxe.registerContract` no longer takes an artifact
13+
14+
Registering classes and instances are now separate, unvalidated operations. `registerContractClass(artifact)` registers a class, `registerContract(instance)` registers an instance and no longer takes an artifact. `registerContract` does not check that PXE knows the contract's artifact: a missing artifact surfaces only when the contract is later simulated.
15+
16+
**Migration:**
17+
18+
- `pxe.registerContract` now takes the instance directly (its address preimage) and returns the derived address. Register the class separately via `registerContractClass`:
19+
20+
```diff
21+
- await pxe.registerContract({ instance, artifact });
22+
+ await pxe.registerContractClass(artifact);
23+
+ await pxe.registerContract(instance);
24+
```
25+
26+
If you were calling it without an artifact, just drop the wrapping object: `pxe.registerContract({ instance })` becomes `pxe.registerContract(instance)`. The `wallet.registerContract(instance, artifact?, secretKeyOrKeys?)` convenience is unchanged and performs both registrations for you.
27+
28+
- To make a new class's code available after an onchain upgrade, register the new artifact instead of calling `updateContract`:
29+
30+
```diff
31+
- await pxe.updateContract(address, newArtifact);
32+
+ await pxe.registerContractClass(newArtifact);
33+
```
34+
35+
The new class is used automatically once the upgrade takes effect on chain; no further PXE action is needed. Registering it beforehand is harmless: until the update activates, the node still resolves the contract's current class to the previous one, so it keeps running its old code.
36+
37+
- `pxe.getContractInstance(address)` and `wallet.getContractMetadata(address).instance` now return the contract's **address preimage**, which no longer includes `currentContractClassId`.
38+
1239
### [Aztec.js] `AccountWithSecretKey` removed, read account keys from the `AccountManager` or PXE
1340

1441
`AccountWithSecretKey` was a thin wrapper that bundled an account's transaction signer with its master secret key, used mainly to print or export the secret. It has been removed, and `AccountManager.getAccount()` now returns the plain `Account` signer. The wrapper's extra methods are no longer available on that value:

docs/examples/webapp-tutorial/src/fees.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export async function getSponsoredFPCContract() {
3030
*/
3131
export async function registerSponsoredFPC(pxe: PXE) {
3232
const contract = await getSponsoredFPCContract();
33-
await pxe.registerContract(contract);
33+
await pxe.registerContractClass(contract.artifact);
34+
await pxe.registerContract(contract.instance);
3435
return contract.instance.address;
3536
}
3637
// docs:end:register-fpc

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

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { Fr } from '@aztec/foundation/curves/bn254';
22
import { AztecAddress } from '@aztec/stdlib/aztec-address';
3-
import {
4-
CompleteAddress,
5-
type ContractInstanceWithAddress,
6-
getContractClassFromArtifact,
7-
} from '@aztec/stdlib/contract';
3+
import { CompleteAddress } from '@aztec/stdlib/contract';
84
import type { TxExecutionRequest, TxReceipt, UtilityExecutionResult } from '@aztec/stdlib/tx';
95
import { OFFCHAIN_MESSAGE_IDENTIFIER } from '@aztec/stdlib/tx';
106

@@ -21,7 +17,6 @@ describe('Contract Class', () => {
2117
let contractAddress: AztecAddress;
2218
let account: MockProxy<Account>;
2319
let accountAddress: CompleteAddress;
24-
let contractInstance: ContractInstanceWithAddress;
2520

2621
const mockTxRequest = { type: 'TxRequest' } as any as TxExecutionRequest;
2722
const mockTxReceipt = { type: 'TxReceipt' } as any as TxReceipt;
@@ -40,17 +35,11 @@ describe('Contract Class', () => {
4035
account = mock<Account>();
4136
accountAddress = await CompleteAddress.random();
4237
account.getCompleteAddress.mockReturnValue(accountAddress);
43-
const contractClass = await getContractClassFromArtifact(testContractArtifact);
44-
contractInstance = {
45-
address: contractAddress,
46-
currentContractClassId: contractClass.id,
47-
originalContractClassId: contractClass.id,
48-
} as ContractInstanceWithAddress;
4938

5039
wallet = mock<Wallet>();
5140
wallet.simulateTx.mockResolvedValue(mockTxSimulationResultWithAppOffset);
5241
account.createTxExecutionRequest.mockResolvedValue(mockTxRequest);
53-
wallet.registerContract.mockResolvedValue(contractInstance);
42+
wallet.registerContract.mockResolvedValue(undefined);
5443
wallet.sendTx.mockResolvedValue({ receipt: mockTxReceipt, offchainEffects: [], offchainMessages: [] });
5544
wallet.executeUtility.mockResolvedValue(mockUtilityResultValue);
5645
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Fr } from '@aztec/foundation/curves/bn254';
22
import { AztecAddress } from '@aztec/stdlib/aztec-address';
3-
import { type ContractInstanceWithAddress, getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
3+
import { getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
44
import { Gas } from '@aztec/stdlib/gas';
55
import { OFFCHAIN_MESSAGE_IDENTIFIER, type OffchainEffect } from '@aztec/stdlib/tx';
66

@@ -16,7 +16,7 @@ describe('DeployMethod', () => {
1616
let wallet: MockProxy<Wallet>;
1717
beforeEach(() => {
1818
wallet = mock<Wallet>();
19-
wallet.registerContract.mockResolvedValue({} as ContractInstanceWithAddress);
19+
wallet.registerContract.mockResolvedValue(undefined);
2020
wallet.getContractClassMetadata.mockResolvedValue({ isContractClassPubliclyRegistered: true } as any);
2121
wallet.getContractMetadata.mockResolvedValue({ isContractPubliclyDeployed: true } as any);
2222
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { SimulationOverrides } from '@aztec/stdlib/tx';
1111

1212
/**
1313
* Builds `SimulationOverrides` that simulate a deployed instance as if it had already been upgraded to a
14-
* new contract class. Mirrors a real on-chain upgrade (`pxe.updateContract` followed by waiting out the delay):
14+
* new contract class. Mirrors a real on-chain upgrade (scheduling the new class and waiting out the delay):
1515
*
1616
* - `publicStorage` rewrites the `ContractInstanceRegistry`'s delayed-public-mutable storage so the AVM's
1717
* `UpdateCheck` resolves to the new class id.

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

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { EventSelector, FunctionCall, FunctionSelector, FunctionType } from '@az
77
import { AuthWitness } from '@aztec/stdlib/auth-witness';
88
import { AztecAddress } from '@aztec/stdlib/aztec-address';
99
import { BlockHash } from '@aztec/stdlib/block';
10-
import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract';
10+
import type { ContractInstancePreimageWithAddress } from '@aztec/stdlib/contract';
1111
import { PublicKeys } from '@aztec/stdlib/keys';
1212
import {
1313
DroppedTxReceipt,
@@ -134,29 +134,17 @@ describe('WalletSchema', () => {
134134
fileMap: {},
135135
storageLayout: {},
136136
};
137-
const mockInstance: ContractInstanceWithAddress = {
137+
const mockInstance: ContractInstancePreimageWithAddress = {
138138
address: await AztecAddress.random(),
139139
version: 2,
140140
salt: Fr.random(),
141141
deployer: await AztecAddress.random(),
142-
currentContractClassId: Fr.random(),
143142
originalContractClassId: Fr.random(),
144143
initializationHash: Fr.random(),
145144
immutablesHash: Fr.random(),
146145
publicKeys: PublicKeys.default(),
147146
};
148-
const result = await context.client.registerContract(mockInstance, mockArtifact, Fr.random());
149-
expect(result).toEqual({
150-
address: expect.any(AztecAddress),
151-
currentContractClassId: expect.any(Fr),
152-
deployer: expect.any(AztecAddress),
153-
initializationHash: expect.any(Fr),
154-
immutablesHash: expect.any(Fr),
155-
originalContractClassId: expect.any(Fr),
156-
publicKeys: expect.any(PublicKeys),
157-
salt: expect.any(Fr),
158-
version: 2,
159-
});
147+
await context.client.registerContract(mockInstance, mockArtifact, Fr.random());
160148
});
161149

162150
it('registerContractClass', async () => {
@@ -335,12 +323,11 @@ describe('WalletSchema', () => {
335323
returnTypes: [],
336324
});
337325

338-
const mockInstance: ContractInstanceWithAddress = {
326+
const mockInstance: ContractInstancePreimageWithAddress = {
339327
address: address2,
340328
version: 2,
341329
salt: Fr.random(),
342330
deployer: await AztecAddress.random(),
343-
currentContractClassId: Fr.random(),
344331
originalContractClassId: Fr.random(),
345332
initializationHash: Fr.random(),
346333
immutablesHash: Fr.random(),
@@ -397,10 +384,7 @@ describe('WalletSchema', () => {
397384
expect(results[4]).toEqual({ name: 'registerSender', result: expect.any(AztecAddress) });
398385
expect(results[5]).toEqual({ name: 'getAddressBook', result: expect.any(Array) });
399386
expect(results[6]).toEqual({ name: 'getAccounts', result: expect.any(Array) });
400-
expect(results[7]).toEqual({
401-
name: 'registerContract',
402-
result: expect.objectContaining({ address: expect.any(AztecAddress) }),
403-
});
387+
expect(results[7]).toEqual({ name: 'registerContract', result: undefined });
404388
expect(results[8]).toEqual({ name: 'simulateTx', result: expect.any(TxSimulationResultWithAppOffset) });
405389
expect(results[9]).toEqual({ name: 'executeUtility', result: expect.any(UtilityExecutionResult) });
406390
expect(results[10]).toEqual({ name: 'profileTx', result: expect.any(TxProfileResult) });
@@ -471,19 +455,7 @@ class MockWallet implements Wallet {
471455
return [{ alias: 'account1', item: await AztecAddress.random() }];
472456
}
473457

474-
async registerContract(_instanceData: any, _artifact?: any, _secretKey?: Fr): Promise<ContractInstanceWithAddress> {
475-
return {
476-
version: 2,
477-
address: await AztecAddress.random(),
478-
currentContractClassId: Fr.random(),
479-
deployer: await AztecAddress.random(),
480-
initializationHash: Fr.random(),
481-
immutablesHash: Fr.random(),
482-
originalContractClassId: Fr.random(),
483-
publicKeys: await PublicKeys.random(),
484-
salt: Fr.random(),
485-
};
486-
}
458+
async registerContract(_instanceData: any, _artifact?: any, _secretKey?: Fr): Promise<void> {}
487459

488460
async registerContractClass(_artifact: any): Promise<void> {}
489461

yarn-project/aztec.js/src/wallet/wallet.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import {
1111
} from '@aztec/stdlib/abi';
1212
import { AuthWitness } from '@aztec/stdlib/auth-witness';
1313
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
14-
import { type ContractInstanceWithAddress, ContractInstanceWithAddressSchema } from '@aztec/stdlib/contract';
14+
import {
15+
type ContractInstancePreimage,
16+
ContractInstancePreimageSchema,
17+
type ContractInstancePreimageWithAddress,
18+
ContractInstancePreimageWithAddressSchema,
19+
} from '@aztec/stdlib/contract';
1520
import { Gas, ManaUsageEstimate } from '@aztec/stdlib/gas';
1621
import type { MasterSecretKeys } from '@aztec/stdlib/keys';
1722
import { refineTxHashAndRange } from '@aztec/stdlib/logs';
@@ -234,8 +239,8 @@ export enum ContractInitializationStatus {
234239
* Contract metadata including deployment and registration status.
235240
*/
236241
export type ContractMetadata = {
237-
/** The contract instance */
238-
instance?: ContractInstanceWithAddress;
242+
/** The contract instance preimage and address. */
243+
instance?: ContractInstancePreimageWithAddress;
239244
/** Whether the contract has been initialized. */
240245
initializationStatus: ContractInitializationStatus;
241246
/** Whether the contract instance is publicly deployed on-chain */
@@ -281,10 +286,10 @@ export type Wallet = {
281286
getAddressBook(): Promise<Aliased<AztecAddress>[]>;
282287
getAccounts(): Promise<Aliased<AztecAddress>[]>;
283288
registerContract(
284-
instance: ContractInstanceWithAddress,
289+
instance: ContractInstancePreimage,
285290
artifact?: ContractArtifact,
286291
secretKeyOrKeys?: Fr | MasterSecretKeys,
287-
): Promise<ContractInstanceWithAddress>;
292+
): Promise<void>;
288293
/**
289294
* Registers a contract class artifact in the local PXE without binding it to any instance.
290295
* Useful for simulation flows that need the artifact available locally before any on-chain
@@ -413,7 +418,7 @@ export const PublicEventSchema: z.ZodType<PublicEvent<AbiDecoded>> = zodFor<Publ
413418
);
414419

415420
export const ContractMetadataSchema = z.object({
416-
instance: optional(ContractInstanceWithAddressSchema),
421+
instance: optional(ContractInstancePreimageWithAddressSchema),
417422
initializationStatus: z.nativeEnum(ContractInitializationStatus),
418423
isContractPublished: z.boolean(),
419424
isContractUpdated: z.boolean(),
@@ -581,8 +586,8 @@ const WalletMethodSchemas = {
581586
output: z.array(z.object({ alias: z.string(), item: schemas.AztecAddress })),
582587
}),
583588
registerContract: z.function({
584-
input: z.tuple([ContractInstanceWithAddressSchema, optional(ContractArtifactSchema), optional(schemas.Fr)]),
585-
output: ContractInstanceWithAddressSchema,
589+
input: z.tuple([ContractInstancePreimageSchema, optional(ContractArtifactSchema), optional(schemas.Fr)]),
590+
output: z.void(),
586591
}),
587592
registerContractClass: z.function({ input: z.tuple([ContractArtifactSchema]), output: z.void() }),
588593
simulateTx: z.function({
@@ -634,12 +639,15 @@ function createBatchSchemas<T extends Record<string, z.ZodFunction<z.ZodTuple<an
634639
}),
635640
);
636641

637-
const namesAndReturns = names.map(name =>
638-
z.object({
642+
const namesAndReturns = names.map(name => {
643+
const returnType = getSchemaReturnType(methodSchemas[name]);
644+
return z.object({
639645
name: z.literal(name),
640-
result: getSchemaReturnType(methodSchemas[name]),
641-
}),
642-
);
646+
// void-returning methods serialize to a missing `result` key over JSON-RPC, so their field must be optional:
647+
// value-returning methods keep it required so a dropped result is still caught.
648+
result: returnType instanceof z.ZodVoid ? returnType.optional() : returnType,
649+
});
650+
});
643651

644652
// Type assertion needed because discriminatedUnion expects a tuple type [T, T, ...T[]]
645653
// but we're building the array dynamically. The runtime behavior is correct.

yarn-project/cli-wallet/src/cmds/check_tx.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,21 +197,22 @@ type ContractArtifactWithClassId = ContractArtifact & { classId: Fr };
197197

198198
async function getKnownArtifacts(wallet: CLIWallet): Promise<ArtifactMap> {
199199
const knownContractAddresses = await wallet.getContracts();
200-
const knownContracts = (
201-
await Promise.all(knownContractAddresses.map(contractAddress => wallet.getContractMetadata(contractAddress)))
202-
).map(contractMetadata => contractMetadata.instance);
203-
const classIds = [...new Set(knownContracts.map(contract => contract?.currentContractClassId))];
200+
const knownContracts = await Promise.all(
201+
knownContractAddresses.map(contractAddress => wallet.getContractMetadata(contractAddress)),
202+
);
203+
const classIdFor = (metadata: (typeof knownContracts)[number]): Fr | undefined =>
204+
metadata.updatedContractClassId ?? metadata.instance?.originalContractClassId;
205+
const classIds = [...new Set(knownContracts.map(classIdFor))];
204206
const knownArtifacts = (
205207
await Promise.all(classIds.map(classId => (classId ? wallet.getContractArtifact(classId) : undefined)))
206208
).map((artifact, index) => (artifact ? { ...artifact, classId: classIds[index] } : undefined));
207209
const map: Record<string, ContractArtifactWithClassId> = {};
208-
for (const instance of knownContracts) {
209-
if (instance) {
210-
const artifact = knownArtifacts.find(a =>
211-
a?.classId?.equals(instance.currentContractClassId),
212-
) as ContractArtifactWithClassId;
210+
for (const metadata of knownContracts) {
211+
const classId = classIdFor(metadata);
212+
if (metadata.instance && classId) {
213+
const artifact = knownArtifacts.find(a => a?.classId?.equals(classId)) as ContractArtifactWithClassId;
213214
if (artifact) {
214-
map[instance.address.toString()] = artifact;
215+
map[metadata.instance.address.toString()] = artifact;
215216
}
216217
}
217218
}

yarn-project/cli-wallet/src/cmds/simulate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ export async function simulate(
4444
if (!metadata.instance) {
4545
return undefined;
4646
}
47-
const artifact = await wallet.getContractArtifact(metadata.instance.currentContractClassId);
47+
const classId = metadata.updatedContractClassId ?? metadata.instance.originalContractClassId;
48+
const artifact = await wallet.getContractArtifact(classId);
4849
return artifact;
4950
},
5051
log,

0 commit comments

Comments
 (0)