Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Private calls and initialization of undeployed contracts #4362

Merged
merged 4 commits into from
Feb 1, 2024
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
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);
});
Comment on lines +234 to +242
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 this is awesome

});

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 @@ -543,15 +542,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
Loading