Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/docs-developers/docs/aztec-js/how_to_test.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Use this to set up state preconditions, reproduce production bugs against pinned

### Fast-forwarding a contract update
Comment thread
nventuro marked this conversation as resolved.

`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.
`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.

```typescript
import { fastForwardContractUpdate } from '@aztec/aztec.js';
Expand Down
27 changes: 27 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [PXE] `pxe.updateContract` removed and `pxe.registerContract` no longer takes an artifact

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.

**Migration:**

- `pxe.registerContract` now takes the instance directly (its address preimage) and returns the derived address. Register the class separately via `registerContractClass`:

```diff
- await pxe.registerContract({ instance, artifact });
+ await pxe.registerContractClass(artifact);
+ await pxe.registerContract(instance);
```

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.

- To make a new class's code available after an onchain upgrade, register the new artifact instead of calling `updateContract`:

```diff
- await pxe.updateContract(address, newArtifact);
+ await pxe.registerContractClass(newArtifact);
```

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.

- `pxe.getContractInstance(address)` and `wallet.getContractMetadata(address).instance` now return the contract's **address preimage**, which no longer includes `currentContractClassId`.

### [Aztec.js] `AccountWithSecretKey` removed, read account keys from the `AccountManager` or PXE

`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:
Expand Down
3 changes: 2 additions & 1 deletion docs/examples/webapp-tutorial/src/fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export async function getSponsoredFPCContract() {
*/
export async function registerSponsoredFPC(pxe: PXE) {
const contract = await getSponsoredFPCContract();
await pxe.registerContract(contract);
await pxe.registerContractClass(contract.artifact);
await pxe.registerContract(contract.instance);
return contract.instance.address;
}
// docs:end:register-fpc
Expand Down
15 changes: 2 additions & 13 deletions yarn-project/aztec.js/src/contract/contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Fr } from '@aztec/foundation/curves/bn254';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import {
CompleteAddress,
type ContractInstanceWithAddress,
getContractClassFromArtifact,
} from '@aztec/stdlib/contract';
import { CompleteAddress } from '@aztec/stdlib/contract';
import type { TxExecutionRequest, TxReceipt, UtilityExecutionResult } from '@aztec/stdlib/tx';
import { OFFCHAIN_MESSAGE_IDENTIFIER } from '@aztec/stdlib/tx';

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

const mockTxRequest = { type: 'TxRequest' } as any as TxExecutionRequest;
const mockTxReceipt = { type: 'TxReceipt' } as any as TxReceipt;
Expand All @@ -40,17 +35,11 @@ describe('Contract Class', () => {
account = mock<Account>();
accountAddress = await CompleteAddress.random();
account.getCompleteAddress.mockReturnValue(accountAddress);
const contractClass = await getContractClassFromArtifact(testContractArtifact);
contractInstance = {
address: contractAddress,
currentContractClassId: contractClass.id,
originalContractClassId: contractClass.id,
} as ContractInstanceWithAddress;

wallet = mock<Wallet>();
wallet.simulateTx.mockResolvedValue(mockTxSimulationResultWithAppOffset);
account.createTxExecutionRequest.mockResolvedValue(mockTxRequest);
wallet.registerContract.mockResolvedValue(contractInstance);
wallet.registerContract.mockResolvedValue(undefined);
wallet.sendTx.mockResolvedValue({ receipt: mockTxReceipt, offchainEffects: [], offchainMessages: [] });
wallet.executeUtility.mockResolvedValue(mockUtilityResultValue);
});
Expand Down
4 changes: 2 additions & 2 deletions yarn-project/aztec.js/src/contract/deploy_method.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Fr } from '@aztec/foundation/curves/bn254';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { type ContractInstanceWithAddress, getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
import { getContractInstanceFromInstantiationParams } from '@aztec/stdlib/contract';
import { Gas } from '@aztec/stdlib/gas';
import { OFFCHAIN_MESSAGE_IDENTIFIER, type OffchainEffect } from '@aztec/stdlib/tx';

Expand All @@ -16,7 +16,7 @@ describe('DeployMethod', () => {
let wallet: MockProxy<Wallet>;
beforeEach(() => {
wallet = mock<Wallet>();
wallet.registerContract.mockResolvedValue({} as ContractInstanceWithAddress);
wallet.registerContract.mockResolvedValue(undefined);
wallet.getContractClassMetadata.mockResolvedValue({ isContractClassPubliclyRegistered: true } as any);
wallet.getContractMetadata.mockResolvedValue({ isContractPubliclyDeployed: true } as any);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { SimulationOverrides } from '@aztec/stdlib/tx';

/**
* Builds `SimulationOverrides` that simulate a deployed instance as if it had already been upgraded to a
* new contract class. Mirrors a real on-chain upgrade (`pxe.updateContract` followed by waiting out the delay):
* new contract class. Mirrors a real on-chain upgrade (scheduling the new class and waiting out the delay):
*
* - `publicStorage` rewrites the `ContractInstanceRegistry`'s delayed-public-mutable storage so the AVM's
* `UpdateCheck` resolves to the new class id.
Expand Down
40 changes: 6 additions & 34 deletions yarn-project/aztec.js/src/wallet/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { EventSelector, FunctionCall, FunctionSelector, FunctionType } from '@az
import { AuthWitness } from '@aztec/stdlib/auth-witness';
import { AztecAddress } from '@aztec/stdlib/aztec-address';
import { BlockHash } from '@aztec/stdlib/block';
import type { ContractInstanceWithAddress } from '@aztec/stdlib/contract';
import type { ContractInstancePreimageWithAddress } from '@aztec/stdlib/contract';
import { PublicKeys } from '@aztec/stdlib/keys';
import {
DroppedTxReceipt,
Expand Down Expand Up @@ -134,29 +134,17 @@ describe('WalletSchema', () => {
fileMap: {},
storageLayout: {},
};
const mockInstance: ContractInstanceWithAddress = {
const mockInstance: ContractInstancePreimageWithAddress = {
address: await AztecAddress.random(),
version: 2,
salt: Fr.random(),
deployer: await AztecAddress.random(),
currentContractClassId: Fr.random(),
originalContractClassId: Fr.random(),
initializationHash: Fr.random(),
immutablesHash: Fr.random(),
publicKeys: PublicKeys.default(),
};
const result = await context.client.registerContract(mockInstance, mockArtifact, Fr.random());
expect(result).toEqual({
address: expect.any(AztecAddress),
currentContractClassId: expect.any(Fr),
deployer: expect.any(AztecAddress),
initializationHash: expect.any(Fr),
immutablesHash: expect.any(Fr),
originalContractClassId: expect.any(Fr),
publicKeys: expect.any(PublicKeys),
salt: expect.any(Fr),
version: 2,
});
await context.client.registerContract(mockInstance, mockArtifact, Fr.random());
});

it('registerContractClass', async () => {
Expand Down Expand Up @@ -335,12 +323,11 @@ describe('WalletSchema', () => {
returnTypes: [],
});

const mockInstance: ContractInstanceWithAddress = {
const mockInstance: ContractInstancePreimageWithAddress = {
address: address2,
version: 2,
salt: Fr.random(),
deployer: await AztecAddress.random(),
currentContractClassId: Fr.random(),
originalContractClassId: Fr.random(),
initializationHash: Fr.random(),
immutablesHash: Fr.random(),
Expand Down Expand Up @@ -397,10 +384,7 @@ describe('WalletSchema', () => {
expect(results[4]).toEqual({ name: 'registerSender', result: expect.any(AztecAddress) });
expect(results[5]).toEqual({ name: 'getAddressBook', result: expect.any(Array) });
expect(results[6]).toEqual({ name: 'getAccounts', result: expect.any(Array) });
expect(results[7]).toEqual({
name: 'registerContract',
result: expect.objectContaining({ address: expect.any(AztecAddress) }),
});
expect(results[7]).toEqual({ name: 'registerContract', result: undefined });
expect(results[8]).toEqual({ name: 'simulateTx', result: expect.any(TxSimulationResultWithAppOffset) });
expect(results[9]).toEqual({ name: 'executeUtility', result: expect.any(UtilityExecutionResult) });
expect(results[10]).toEqual({ name: 'profileTx', result: expect.any(TxProfileResult) });
Expand Down Expand Up @@ -471,19 +455,7 @@ class MockWallet implements Wallet {
return [{ alias: 'account1', item: await AztecAddress.random() }];
}

async registerContract(_instanceData: any, _artifact?: any, _secretKey?: Fr): Promise<ContractInstanceWithAddress> {
return {
version: 2,
address: await AztecAddress.random(),
currentContractClassId: Fr.random(),
deployer: await AztecAddress.random(),
initializationHash: Fr.random(),
immutablesHash: Fr.random(),
originalContractClassId: Fr.random(),
publicKeys: await PublicKeys.random(),
salt: Fr.random(),
};
}
async registerContract(_instanceData: any, _artifact?: any, _secretKey?: Fr): Promise<void> {}

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

Expand Down
34 changes: 21 additions & 13 deletions yarn-project/aztec.js/src/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import {
} from '@aztec/stdlib/abi';
import { AuthWitness } from '@aztec/stdlib/auth-witness';
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
import { type ContractInstanceWithAddress, ContractInstanceWithAddressSchema } from '@aztec/stdlib/contract';
import {
type ContractInstancePreimage,
ContractInstancePreimageSchema,
type ContractInstancePreimageWithAddress,
ContractInstancePreimageWithAddressSchema,
} from '@aztec/stdlib/contract';
import { Gas, ManaUsageEstimate } from '@aztec/stdlib/gas';
import type { MasterSecretKeys } from '@aztec/stdlib/keys';
import { refineTxHashAndRange } from '@aztec/stdlib/logs';
Expand Down Expand Up @@ -234,8 +239,8 @@ export enum ContractInitializationStatus {
* Contract metadata including deployment and registration status.
*/
export type ContractMetadata = {
/** The contract instance */
instance?: ContractInstanceWithAddress;
/** The contract instance preimage and address. */
instance?: ContractInstancePreimageWithAddress;
/** Whether the contract has been initialized. */
initializationStatus: ContractInitializationStatus;
/** Whether the contract instance is publicly deployed on-chain */
Expand Down Expand Up @@ -281,10 +286,10 @@ export type Wallet = {
getAddressBook(): Promise<Aliased<AztecAddress>[]>;
getAccounts(): Promise<Aliased<AztecAddress>[]>;
registerContract(
instance: ContractInstanceWithAddress,
instance: ContractInstancePreimage,
artifact?: ContractArtifact,
secretKeyOrKeys?: Fr | MasterSecretKeys,
): Promise<ContractInstanceWithAddress>;
): Promise<void>;
/**
* Registers a contract class artifact in the local PXE without binding it to any instance.
* Useful for simulation flows that need the artifact available locally before any on-chain
Expand Down Expand Up @@ -413,7 +418,7 @@ export const PublicEventSchema: z.ZodType<PublicEvent<AbiDecoded>> = zodFor<Publ
);

export const ContractMetadataSchema = z.object({
instance: optional(ContractInstanceWithAddressSchema),
instance: optional(ContractInstancePreimageWithAddressSchema),
initializationStatus: z.nativeEnum(ContractInitializationStatus),
isContractPublished: z.boolean(),
isContractUpdated: z.boolean(),
Expand Down Expand Up @@ -581,8 +586,8 @@ const WalletMethodSchemas = {
output: z.array(z.object({ alias: z.string(), item: schemas.AztecAddress })),
}),
registerContract: z.function({
input: z.tuple([ContractInstanceWithAddressSchema, optional(ContractArtifactSchema), optional(schemas.Fr)]),
output: ContractInstanceWithAddressSchema,
input: z.tuple([ContractInstancePreimageSchema, optional(ContractArtifactSchema), optional(schemas.Fr)]),
output: z.void(),
}),
registerContractClass: z.function({ input: z.tuple([ContractArtifactSchema]), output: z.void() }),
simulateTx: z.function({
Expand Down Expand Up @@ -634,12 +639,15 @@ function createBatchSchemas<T extends Record<string, z.ZodFunction<z.ZodTuple<an
}),
);

const namesAndReturns = names.map(name =>
z.object({
const namesAndReturns = names.map(name => {
const returnType = getSchemaReturnType(methodSchemas[name]);
return z.object({
name: z.literal(name),
result: getSchemaReturnType(methodSchemas[name]),
}),
);
// void-returning methods serialize to a missing `result` key over JSON-RPC, so their field must be optional:
// value-returning methods keep it required so a dropped result is still caught.
result: returnType instanceof z.ZodVoid ? returnType.optional() : returnType,
});
});

// Type assertion needed because discriminatedUnion expects a tuple type [T, T, ...T[]]
// but we're building the array dynamically. The runtime behavior is correct.
Expand Down
21 changes: 11 additions & 10 deletions yarn-project/cli-wallet/src/cmds/check_tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,21 +197,22 @@ type ContractArtifactWithClassId = ContractArtifact & { classId: Fr };

async function getKnownArtifacts(wallet: CLIWallet): Promise<ArtifactMap> {
const knownContractAddresses = await wallet.getContracts();
const knownContracts = (
await Promise.all(knownContractAddresses.map(contractAddress => wallet.getContractMetadata(contractAddress)))
).map(contractMetadata => contractMetadata.instance);
const classIds = [...new Set(knownContracts.map(contract => contract?.currentContractClassId))];
const knownContracts = await Promise.all(
knownContractAddresses.map(contractAddress => wallet.getContractMetadata(contractAddress)),
);
const classIdFor = (metadata: (typeof knownContracts)[number]): Fr | undefined =>
metadata.updatedContractClassId ?? metadata.instance?.originalContractClassId;
const classIds = [...new Set(knownContracts.map(classIdFor))];
const knownArtifacts = (
await Promise.all(classIds.map(classId => (classId ? wallet.getContractArtifact(classId) : undefined)))
).map((artifact, index) => (artifact ? { ...artifact, classId: classIds[index] } : undefined));
const map: Record<string, ContractArtifactWithClassId> = {};
for (const instance of knownContracts) {
if (instance) {
const artifact = knownArtifacts.find(a =>
a?.classId?.equals(instance.currentContractClassId),
) as ContractArtifactWithClassId;
for (const metadata of knownContracts) {
const classId = classIdFor(metadata);
if (metadata.instance && classId) {
const artifact = knownArtifacts.find(a => a?.classId?.equals(classId)) as ContractArtifactWithClassId;
if (artifact) {
map[instance.address.toString()] = artifact;
map[metadata.instance.address.toString()] = artifact;
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion yarn-project/cli-wallet/src/cmds/simulate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export async function simulate(
if (!metadata.instance) {
return undefined;
}
const artifact = await wallet.getContractArtifact(metadata.instance.currentContractClassId);
const classId = metadata.updatedContractClassId ?? metadata.instance.originalContractClassId;
const artifact = await wallet.getContractArtifact(classId);
return artifact;
},
log,
Expand Down
3 changes: 2 additions & 1 deletion yarn-project/cli-wallet/src/utils/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export class CLIWallet extends BaseWallet {

private async registerAuthRegistry(): Promise<void> {
const { instance, artifact } = await getStandardAuthRegistry();
await this.pxe.registerContract({ instance, artifact });
await this.pxe.registerContractClass(artifact);
await this.pxe.registerContract(instance);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,16 @@ describe('automine/contracts/contract_updates', () => {
).rejects.toThrow('New update delay is too low');
});

// Tries to register the instance against UpdatedContract.artifact before the upgrade window passes;
// expects the PXE to reject with a class mismatch error.
it('should not allow to instantiate a contract with an updated class before the update happens', async () => {
await expect(wallet.registerContract(instance, UpdatedContract.artifact)).rejects.toThrow(
'Could not update contract to a class different from the current one',
);
it('permits registering the updated artifact before the update, but still runs the original class', async () => {
Comment thread
nventuro marked this conversation as resolved.
// Registration performs no validation, so the updated artifact can be registered before the upgrade activates.
await expect(wallet.registerContract(instance, UpdatedContract.artifact)).resolves.not.toThrow();

// The node still resolves the contract's current class to the original until the update is enacted, so a call
// against the updated class's (no-arg) set_public_value selector does not resolve against the deployed class.
const updatedContract = UpdatedContract.at(contract.address, wallet);
await expect(
updatedContract.methods.set_public_value().simulate({ from: defaultAccountAddress }),
).rejects.toThrow();
});

// UpdatableContract's `set_public_value(Field)` and UpdatedContract's `set_public_value()`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('automine/contracts/deploy/deploy_method', () => {
);
// docs:start:verify_deployment
const metadata = await wallet.getContractMetadata(contract.address);
const classMetadata = await wallet.getContractClassMetadata(metadata.instance!.currentContractClassId);
const classMetadata = await wallet.getContractClassMetadata(metadata.instance!.originalContractClassId);
const isPublished = classMetadata.isContractClassPubliclyRegistered;
// docs:end:verify_deployment
expect(isPublished).toBeTrue();
Expand Down
Loading
Loading