Skip to content
Open
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
23 changes: 19 additions & 4 deletions yarn-project/aztec/src/deploy/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ describe('runDeployment', () => {
}
}, 300_000);

it('auto-derives interdependencies, dedupes a shared class, and registers privately', async () => {
it('auto-derives interdependencies, dedupes a shared class, registers privately, and deploys universally', async () => {
const secret = Fr.fromString('0x00000000000000000000000000000000000000000000000000000000feedface');
const salt = new Fr(0);
const admin = await getSchnorrInitializerlessAccountContractAddress(
Expand Down Expand Up @@ -234,11 +234,20 @@ describe('runDeployment', () => {
mode: 'register',
initializerArgs: r => [r.account('admin'), 'Reg', 'REG', 18],
},
// Universal: `from` sends and pays, but the address preimage omits it.
uniToken: {
kind: 'contract',
contract: TokenContract,
from: r => r.account('admin'),
mode: 'publish',
universal: true,
initializerArgs: r => [r.account('admin'), 'Universal', 'UNI', 18],
},
} satisfies Record<string, ContractStep>;

const addresses: Record<string, string> = {};
const capture = (ctx: Ctx) => {
for (const alias of ['token', 'token2', 'regToken']) {
for (const alias of ['token', 'token2', 'regToken', 'uniToken']) {
addresses[alias] = ctx.contract(alias).toString();
}
};
Expand All @@ -260,8 +269,8 @@ describe('runDeployment', () => {

// Interdependency: token2's args reference token, so its dependency is auto-derived.
expect(plan?.steps.find(s => s.id === 'token2')?.dependsOn).toContain('token');
// All three resolved to distinct deterministic addresses.
expect(new Set(Object.values(addresses)).size).toBe(3);
// All four resolved to distinct deterministic addresses.
expect(new Set(Object.values(addresses)).size).toBe(4);
// The register step must honor its declared deployer in the derivation — the registered
// address has to match what publishing the same spec (deployer included) would produce.
const expectedRegToken = await getContractInstanceFromInstantiationParams(TokenContract.artifact, {
Expand All @@ -270,6 +279,12 @@ describe('runDeployment', () => {
deployer: admin,
});
expect(addresses.regToken).toEqual(expectedRegToken.address.toString());
// The universal step's address must omit the deployer from the preimage.
const expectedUniToken = await getContractInstanceFromInstantiationParams(TokenContract.artifact, {
salt: new Fr(0),
constructorArgs: [admin, 'Universal', 'UNI', 18],
});
expect(addresses.uniToken).toEqual(expectedUniToken.address.toString());

// Idempotent re-run: both publishes are on-chain and the register step sends no tx, so the
// whole graph is a no-op. (A broken shared-class dedup would have thrown on the first run.)
Expand Down
23 changes: 16 additions & 7 deletions yarn-project/aztec/src/deploy/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ function isDeferred<C>(step: ContractStep<C>): boolean {
return step.deferredInitializerArgs != null;
}

/** The account that sends (and pays for) a contract step's publish tx. */
function contractSender<C>(step: ContractStep<C>): (resolve: Resolver) => AztecAddress {
return step.universal ? step.from : step.deployer;
}

/**
* The contract aliases a pure resolver callback looks up, extracted by dry-running it against a
* resolver that records each `contract(alias)` lookup instead of resolving it. The callback's return
Expand Down Expand Up @@ -347,7 +352,7 @@ class DeploymentRun<C extends Steps> {
const accountUsedBy = new Set<string>();
for (const alias of this.execAliases) {
const step = getOrThrow(this.steps, alias, 'step');
const address = step.kind === 'contract' ? step.deployer(this.resolver) : step.from(this.resolver);
const address = step.kind === 'contract' ? contractSender(step)(this.resolver) : step.from(this.resolver);
accountUsedBy.add(address.toString());
}
const stepStatus = (alias: string, step: StepSpec<C>): DeployPlan['steps'][number]['status'] => {
Expand Down Expand Up @@ -503,13 +508,13 @@ class DeploymentRun<C extends Steps> {
/**
* Publishes/registers a contract from already-computed initializer args (used upfront for
* deterministic contracts, and at inventory or execution time for deferred ones). Both modes
* derive the address from the full instantiation params — deployer and secret-derived public
* keys included — so a registered contract lands on the same address publishing it would.
* derive the address from the full instantiation params — deployer (unless universal) and
* secret-derived public keys included — so a registered contract lands on the same address
* publishing it would.
*/
private async resolveContract(alias: string, step: ContractStep<C>, args: unknown[]): Promise<void> {
this.publishedCache.delete(alias); // re-resolution may change the address
const salt = step.salt ?? this.defaultSalt;
const deployer = step.deployer(this.resolver);
const publicKeys = step.secret ? (await deriveKeys(step.secret)).publicKeys : undefined;
if (step.mode === 'publish') {
const deployMethod = DeployMethod.create<ContractBase>(
Expand All @@ -520,7 +525,11 @@ class DeploymentRun<C extends Steps> {
args,
...(step.initializer ? { constructorNameOrArtifact: step.initializer } : {}),
},
{ deployer, salt, ...(publicKeys ? { publicKeys } : {}) },
{
...(step.universal ? { universalDeploy: true } : { deployer: step.deployer(this.resolver) }),
salt,
...(publicKeys ? { publicKeys } : {}),
},
);
const instance = await deployMethod.getInstance();
this.contractAddresses.set(alias, instance.address);
Expand All @@ -530,7 +539,7 @@ class DeploymentRun<C extends Steps> {
} else {
const instance = await getContractInstanceFromInstantiationParams(step.contract.artifact, {
salt,
deployer,
...(step.universal ? {} : { deployer: step.deployer(this.resolver) }),
...(publicKeys ? { publicKeys } : {}),
...(args.length ? { constructorArgs: args } : {}),
...(step.initializer ? { constructorArtifact: step.initializer } : {}),
Expand Down Expand Up @@ -767,7 +776,7 @@ class DeploymentRun<C extends Steps> {
const units: ExecutionUnit[] = [];
for (const alias of layer.filter(a => this.contractSteps.has(a))) {
const step = getOrThrow(this.contractSteps, alias, 'contract');
const account = step.deployer(this.resolver);
const account = contractSender(step)(this.resolver);
units.push({
label: `publish ${alias}`,
kind: 'publish',
Expand Down
32 changes: 26 additions & 6 deletions yarn-project/aztec/src/deploy/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,32 @@ export interface GeneratedContractClass<T extends ContractBase = ContractBase> {
at(address: AztecAddress, wallet: Wallet): T;
}

/**
* Who sends a contract step's deploy, and whether the address commits to them:
* - `deployer` — a bound deploy: the account salts + sends, and the address commits to it.
* - `universal: true` + `from` — a universal deploy: the address is the same whoever sends it, so
* it can be shared before anyone commits to deploying it; `from` sends and pays.
*/
type ContractSender =
| {
/** Account that salts + sends the deploy, e.g. `(r) => r.account("admin")`. */
deployer: (resolve: Resolver) => AztecAddress;
universal?: never;
from?: never;
}
| {
/** Publish without binding the sender into the address. */
universal: true;
/** Account that sends (and pays for) the deploy, e.g. `(r) => r.account("admin")`. */
from: (resolve: Resolver) => AztecAddress;
deployer?: never;
};

/** What a contract step declares however its initializer args are produced. */
interface ContractStepBase<T extends ContractBase = ContractBase> {
type ContractStepBase<T extends ContractBase = ContractBase> = ContractSender & {
kind: 'contract';
/** The generated contract class — provides the artifact and the typed `.at`. */
contract: GeneratedContractClass<T>;
/** Account that salts + sends the deploy, e.g. `(r) => r.account("admin")`. */
deployer: (resolve: Resolver) => AztecAddress;
/** Per-contract salt, overriding {@link DeploymentSpec.salt}. */
salt?: Fr;
/**
Expand All @@ -83,15 +102,16 @@ interface ContractStepBase<T extends ContractBase = ContractBase> {
secret?: Fr;
/** Name of a non-default `#[initializer]` to call. Defaults to the contract's constructor. */
initializer?: string;
}
};

/**
* A step that puts a contract on-chain (or in the PXE):
* - `publish` → register the class + deploy the instance + run its initializer (a tx).
* - `register` → private; only derive the deterministic address and register it in the PXE (no tx).
*
* The address is deterministic in (class id, deployer, salt, initializer + its args). The two ways
* to supply those args are the union's two variants, so a step can only pick one.
* The address is deterministic in (class id, deployer, salt, initializer + its args) — with the
* deployer omitted for {@link ContractSender | universal} steps. The two ways to supply those args
* are the union's two variants, so a step can only pick one.
*/
export type ContractStep<C = Steps, T extends ContractBase = ContractBase> =
| (ContractStepBase<T> & {
Expand Down
Loading