Skip to content

Commit

Permalink
feat: Private calls and initialization of undeployed contracts (#4362)
Browse files Browse the repository at this point in the history
Adds final tweaks to kernel circuit and e2e tests to check that a
contract private function can be called without needing to initialize or
deploy the contract, and to check that a contract can be privately
initialized without deploying it.

Fixes #4057
Fixes #4058
Fixes #4059
  • Loading branch information
spalladino authored Feb 1, 2024
1 parent 13e0683 commit f31c181
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,18 @@ describe('Private Execution test suite', () => {
oracle.getFunctionArtifactByName.mockImplementation((_, functionName: string) =>
Promise.resolve(getFunctionArtifact(StatefulTestContractArtifact, functionName)),
);

oracle.getFunctionArtifact.mockImplementation((_, selector: FunctionSelector) =>
Promise.resolve(getFunctionArtifact(StatefulTestContractArtifact, selector)),
);

oracle.getPortalContractAddress.mockResolvedValue(EthAddress.ZERO);
});

it('should have a constructor with arguments that inserts notes', async () => {
const artifact = getFunctionArtifact(StatefulTestContractArtifact, 'constructor');
const result = await runSimulator({ args: [owner, 140], artifact });
const topLevelResult = await runSimulator({ args: [owner, 140], artifact });
const result = topLevelResult.nestedExecutions[0];

expect(result.newNotes).toHaveLength(1);
const newNote = result.newNotes[0];
Expand Down
3 changes: 1 addition & 2 deletions yarn-project/aztec.js/src/contract/deploy_method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
ContractDeploymentData,
FunctionData,
TxContext,
computeContractAddressFromInstance,
computePartialAddress,
getContractInstanceFromDeployParams,
} from '@aztec/circuits.js';
Expand Down Expand Up @@ -77,7 +76,7 @@ export class DeployMethod<TContract extends ContractBase = Contract> extends Bas

const deployParams = [this.artifact, this.args, contractAddressSalt, this.publicKey, portalContract] as const;
const instance = getContractInstanceFromDeployParams(...deployParams);
const address = computeContractAddressFromInstance(instance);
const address = instance.address;

const contractDeploymentData = new ContractDeploymentData(
this.publicKey,
Expand Down
10 changes: 5 additions & 5 deletions yarn-project/circuits.js/src/contract/contract_instance.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ContractArtifact } from '@aztec/foundation/abi';
import { ContractInstance, ContractInstanceWithAddress } from '@aztec/types/contracts';

import { EthAddress, Fr, PublicKey, computeContractClassId, getContractClassFromArtifact } from '../index.js';
import { EthAddress, Fr, Point, PublicKey, computeContractClassId, getContractClassFromArtifact } from '../index.js';
import {
computeContractAddressFromInstance,
computeInitializationHash,
Expand All @@ -20,10 +20,10 @@ import { isConstructor } from './contract_tree/contract_tree.js';
*/
export function getContractInstanceFromDeployParams(
artifact: ContractArtifact,
args: any[],
contractAddressSalt: Fr,
publicKey: PublicKey,
portalContractAddress: EthAddress,
args: any[] = [],
contractAddressSalt: Fr = Fr.random(),
publicKey: PublicKey = Point.ZERO,
portalContractAddress: EthAddress = EthAddress.ZERO,
): ContractInstanceWithAddress {
const constructorArtifact = artifact.functions.find(isConstructor);
if (!constructorArtifact) {
Expand Down
8 changes: 7 additions & 1 deletion yarn-project/circuits.js/src/structs/complete_address.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { Fr, Point } from '@aztec/foundation/fields';
import { Fr, GrumpkinScalar, Point } from '@aztec/foundation/fields';
import { BufferReader } from '@aztec/foundation/serialize';

import { Grumpkin } from '../barretenberg/index.js';
Expand Down Expand Up @@ -48,6 +48,12 @@ export class CompleteAddress {
return new CompleteAddress(address, publicKey, partialAddress);
}

static fromRandomPrivateKey() {
const privateKey = GrumpkinScalar.random();
const partialAddress = Fr.random();
return { privateKey, completeAddress: CompleteAddress.fromPrivateKeyAndPartialAddress(privateKey, partialAddress) };
}

static fromPrivateKeyAndPartialAddress(privateKey: GrumpkinPrivateKey, partialAddress: Fr): CompleteAddress {
const grumpkin = new Grumpkin();
const publicKey = grumpkin.mul(Grumpkin.generator, privateKey);
Expand Down
66 changes: 65 additions & 1 deletion yarn-project/end-to-end/src/e2e_deploy_contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import {
AztecAddress,
BatchCall,
CompleteAddress,
Contract,
ContractArtifact,
ContractDeployer,
DebugLogger,
EthAddress,
Fr,
PXE,
SignerlessWallet,
TxStatus,
Wallet,
getContractInstanceFromDeployParams,
isContractDeployed,
} from '@aztec/aztec.js';
import { TestContractArtifact } from '@aztec/noir-contracts/Test';
import { siloNullifier } from '@aztec/circuits.js/abis';
import { StatefulTestContract } from '@aztec/noir-contracts';
import { TestContract, TestContractArtifact } from '@aztec/noir-contracts/Test';
import { TokenContractArtifact } from '@aztec/noir-contracts/Token';
import { SequencerClient } from '@aztec/sequencer-client';

Expand Down Expand Up @@ -195,4 +200,63 @@ describe('e2e_deploy_contract', () => {
});
}
}, 60_000);

// Tests calling a private function in an uninitialized and undeployed contract. Note that
// it still requires registering the contract artifact and instance locally in the pxe.
test.each(['as entrypoint', 'from an account contract'] as const)(
'executes a function in an undeployed contract %s',
async kind => {
const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet;
const contract = await registerContract(testWallet, TestContract);
const receipt = await contract.methods.emit_nullifier(10).send().wait({ debug: true });
const expected = siloNullifier(contract.address, new Fr(10));
expect(receipt.debugInfo?.newNullifiers[1]).toEqual(expected);
},
);

// Tests privately initializing an undeployed contract. Also requires pxe registration in advance.
test.each(['as entrypoint', 'from an account contract'] as const)(
'privately initializes an undeployed contract contract %s',
async kind => {
const testWallet = kind === 'as entrypoint' ? new SignerlessWallet(pxe) : wallet;
const owner = await registerRandomAccount(pxe);
const initArgs: StatefulContractCtorArgs = [owner, 42];
const contract = await registerContract(testWallet, StatefulTestContract, initArgs);
await contract.methods
.constructor(...initArgs)
.send()
.wait();
expect(await contract.methods.summed_values(owner).view()).toEqual(42n);
},
);

// Tests privately initializing multiple undeployed contracts on the same tx through an account contract.
it('initializes multiple undeployed contracts in a single tx', async () => {
const owner = await registerRandomAccount(pxe);
const initArgs: StatefulContractCtorArgs[] = [42, 52].map(value => [owner, value]);
const contracts = await Promise.all(initArgs.map(args => registerContract(wallet, StatefulTestContract, args)));
const calls = contracts.map((c, i) => c.methods.constructor(...initArgs[i]).request());
await new BatchCall(wallet, calls).send().wait();
expect(await contracts[0].methods.summed_values(owner).view()).toEqual(42n);
expect(await contracts[1].methods.summed_values(owner).view()).toEqual(52n);
});
});

type StatefulContractCtorArgs = Parameters<StatefulTestContract['methods']['constructor']>;

async function registerRandomAccount(pxe: PXE): Promise<AztecAddress> {
const { completeAddress: owner, privateKey } = CompleteAddress.fromRandomPrivateKey();
await pxe.registerAccount(privateKey, owner.partialAddress);
return owner.address;
}

type ContractArtifactClass = {
at(address: AztecAddress, wallet: Wallet): Promise<Contract>;
artifact: ContractArtifact;
};

async function registerContract(wallet: Wallet, contractArtifact: ContractArtifactClass, args: any[] = []) {
const instance = getContractInstanceFromDeployParams(contractArtifact.artifact, args);
await wallet.addContracts([{ artifact: contractArtifact.artifact, instance }]);
return contractArtifact.at(instance.address, wallet);
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function generateAbiStatement(name: string, artifactImportPath: string) {
* @returns The corresponding ts code.
*/
export function generateTypescriptContractInterface(input: ContractArtifact, artifactImportPath?: string) {
const methods = input.functions.filter(f => f.name !== 'constructor' && !f.isInternal).map(generateMethod);
const methods = input.functions.filter(f => !f.isInternal).map(generateMethod);
const deploy = artifactImportPath && generateDeploy(input);
const ctor = artifactImportPath && generateConstructor(input.name);
const at = artifactImportPath && generateAt(input.name);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// A contract used for testing a random hodgepodge of small features from simulator and end-to-end tests.
contract StatefulTest {
use dep::aztec::protocol_types::address::AztecAddress;
use dep::aztec::protocol_types::{
address::AztecAddress,
abis::function_selector::FunctionSelector,
};
use dep::std::option::Option;
use dep::value_note::{
balance_utils,
Expand Down Expand Up @@ -47,8 +50,8 @@ contract StatefulTest {

#[aztec(private)]
fn constructor(owner: AztecAddress, value: Field) {
let loc = storage.notes.at(owner);
increment(loc, value, owner);
let selector = FunctionSelector::from_signature("create_note((Field),Field)");
let _res = context.call_private_function(context.this_address(), selector, [owner.to_field(), value]);
}

#[aztec(private)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ impl PrivateKernelInputsInner {
let this_call_stack_item = self.private_call.call_stack_item;
let function_data = this_call_stack_item.function_data;
assert(function_data.is_private, "Private kernel circuit can only execute a private function");
assert(function_data.is_constructor == false, "A constructor must be executed as the first tx in the recursion");
assert(self.previous_kernel.public_inputs.is_private, "Can only verify a private kernel snark in the private kernel circuit");
}

Expand Down Expand Up @@ -542,15 +541,6 @@ mod tests {
builder.failed();
}

#[test(should_fail_with="A constructor must be executed as the first tx in the recursion")]
fn private_function_is_constructor_fails() {
let mut builder = PrivateKernelInnerInputsBuilder::new();

builder.private_call.function_data.is_constructor = true;

builder.failed();
}

#[test(should_fail_with="Can only verify a private kernel snark in the private kernel circuit")]
fn previous_kernel_is_private_false_fails() {
let mut builder = PrivateKernelInnerInputsBuilder::new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class PrivateFunctionsTree {
if (!artifact) {
throw new Error(
`Unknown function. Selector ${selector.toString()} not found in the artifact of contract ${this.contract.instance.address.toString()}. Expected one of: ${this.contract.functions
.map(f => f.selector.toString())
.map(f => `${f.name} (${f.selector.toString()})`)
.join(', ')}`,
);
}
Expand Down

0 comments on commit f31c181

Please sign in to comment.