From b4acc8c6227f1551998aab9a300891b560479b9c Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Wed, 7 Feb 2024 10:40:28 -0300 Subject: [PATCH] feat: Canonical instance deployer contract (#4436) Implementation for the initial contract instance deployer contract. Tracks instance deployed events in nodes and stores them locally. Skips validations for class id and eth address for now. Fixes https://github.com/AztecProtocol/aztec-packages/issues/4071 Fixes https://github.com/AztecProtocol/aztec-packages/issues/4072 --- .../src/core/libraries/ConstantsGen.sol | 2 + .../archiver/src/archiver/archiver.ts | 77 ++++++++++++++- .../aztec-node/src/aztec-node/server.ts | 6 +- .../circuit-types/src/contract_data.ts | 8 +- .../src/interfaces/aztec-node.ts | 8 +- yarn-project/circuits.js/src/constants.gen.ts | 2 + .../src/contract/contract_class.ts | 2 +- .../contract_instance_deployed_event.ts | 68 +++++++++++++ .../circuits.js/src/contract/index.ts | 1 + .../end-to-end/src/cli_docs_sandbox.test.ts | 1 + .../src/e2e_deploy_contract.test.ts | 95 +++++++++++++++++-- yarn-project/foundation/src/abi/buffer.ts | 2 +- yarn-project/foundation/src/fields/fields.ts | 4 + yarn-project/noir-contracts/Nargo.toml | 1 + .../Nargo.toml | 8 ++ .../src/events.nr | 1 + .../src/events/instance_deployed.nr | 36 +++++++ .../src/main.nr | 63 ++++++++++++ .../stateful_test_contract/src/main.nr | 10 ++ .../src/crates/types/src/address.nr | 12 +++ .../src/crates/types/src/constants.nr | 4 + 21 files changed, 394 insertions(+), 17 deletions(-) create mode 100644 yarn-project/circuits.js/src/contract/contract_instance_deployed_event.ts create mode 100644 yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/Nargo.toml create mode 100644 yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/events.nr create mode 100644 yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/events/instance_deployed.nr create mode 100644 yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/main.nr diff --git a/l1-contracts/src/core/libraries/ConstantsGen.sol b/l1-contracts/src/core/libraries/ConstantsGen.sol index 9a90e45a33e..3a0da1d4d75 100644 --- a/l1-contracts/src/core/libraries/ConstantsGen.sol +++ b/l1-contracts/src/core/libraries/ConstantsGen.sol @@ -75,6 +75,8 @@ library Constants { 0x1b70e95fde0b70adc30496b90a327af6a5e383e028e7a43211a07bcd; uint256 internal constant REGISTERER_UNCONSTRAINED_FUNCTION_BROADCASTED_MAGIC_VALUE = 0xe7af816635466f128568edb04c9fa024f6c87fb9010fdbffa68b3d99; + uint256 internal constant DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE = + 0x85864497636cf755ae7bde03f267ce01a520981c21c3682aaf82a631; uint256 internal constant L1_TO_L2_MESSAGE_LENGTH = 8; uint256 internal constant L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH = 25; uint256 internal constant MAX_NOTE_FIELDS_LENGTH = 20; diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 473c3da3abd..3eb5cae25ee 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -22,6 +22,7 @@ import { NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, REGISTERER_CONTRACT_CLASS_REGISTERED_MAGIC_VALUE, } from '@aztec/circuits.js'; +import { ContractInstanceDeployedEvent, computeSaltedInitializationHash } from '@aztec/circuits.js/contract'; import { createEthereumChain } from '@aztec/ethereum'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; @@ -73,12 +74,19 @@ export class Archiver implements ArchiveSource { // TODO(@spalladino): Calculate this on the fly somewhere else. // Today this is printed in the logs for end-to-end test at // end-to-end/src/e2e_deploy_contract.test.ts -t 'registering a new contract class' - // as "Added contract ContractClassRegisterer ADDRESS" + // "Added contract ContractClassRegisterer ADDRESS" + // "Added contract ContractInstanceDeployer ADDRESS" + /** Address of the ClassRegisterer contract with a salt=1 */ private classRegistererAddress = AztecAddress.fromString( '0x29c0cd0000951bba8af520ad5513cc53d9f0413c5a24a72a4ba8c17894c0bef9', ); + /** Address of the InstanceDeployer contract with a salt=1 */ + private instanceDeployerAddress = AztecAddress.fromString( + '0x1799c61aa10430bf6fec46679c4cb76c3ed12cd8b6e73ed7389d5ae296ad1b97', + ); + /** * Creates a new instance of the Archiver. * @param publicClient - A client for interacting with the Ethereum node. @@ -288,13 +296,14 @@ export class Archiver implements ArchiveSource { ), ); - // Unroll all logs emitted during the retrieved blocks and extract any contract classes from them + // Unroll all logs emitted during the retrieved blocks and extract any contract classes and instances from them await Promise.all( retrievedBlocks.retrievedData.map(async block => { const blockLogs = (block.newUnencryptedLogs?.txLogs ?? []) .flatMap(txLog => txLog.unrollLogs()) .map(log => UnencryptedL2Log.fromBuffer(log)); await this.storeRegisteredContractClasses(blockLogs, block.number); + await this.storeDeployedContractInstances(blockLogs, block.number); }), ); @@ -355,6 +364,33 @@ export class Archiver implements ArchiveSource { } } + /** + * Extracts and stores contract instances out of ContractInstanceDeployed events emitted by the canonical deployer contract. + * @param allLogs - All logs emitted in a bunch of blocks. + */ + private async storeDeployedContractInstances(allLogs: UnencryptedL2Log[], blockNum: number) { + const contractInstances: ContractInstanceWithAddress[] = []; + for (const log of allLogs) { + try { + if ( + !log.contractAddress.equals(this.instanceDeployerAddress) || + !ContractInstanceDeployedEvent.isContractInstanceDeployedEvent(log.data) + ) { + continue; + } + const event = ContractInstanceDeployedEvent.fromLogData(log.data); + contractInstances.push(event.toContractInstance()); + } catch (err) { + this.log.warn(`Error processing log ${log.toHumanReadable()}: ${err}`); + } + } + + if (contractInstances.length > 0) { + contractInstances.forEach(c => this.log(`Storing contract instance at ${c.address.toString()}`)); + await this.store.addContractInstances(contractInstances, blockNum); + } + } + /** * Stores extended contract data as classes and instances. * Temporary solution until we source this data from the contract class registerer and instance deployer. @@ -426,8 +462,37 @@ export class Archiver implements ArchiveSource { * @param contractAddress - The contract data address. * @returns The extended contract data or undefined if not found. */ - getExtendedContractData(contractAddress: AztecAddress): Promise { - return this.store.getExtendedContractData(contractAddress); + public async getExtendedContractData(contractAddress: AztecAddress): Promise { + return ( + (await this.store.getExtendedContractData(contractAddress)) ?? this.makeExtendedContractDataFor(contractAddress) + ); + } + + /** + * Temporary method for creating a fake extended contract data out of classes and instances registered in the node. + * Used as a fallback if the extended contract data is not found. + */ + private async makeExtendedContractDataFor(address: AztecAddress): Promise { + const instance = await this.store.getContractInstance(address); + if (!instance) { + return undefined; + } + + const contractClass = await this.store.getContractClass(instance.contractClassId); + if (!contractClass) { + this.log.warn( + `Contract class ${instance.contractClassId.toString()} for address ${address.toString()} not found`, + ); + return undefined; + } + + return new ExtendedContractData( + new ContractData(address, instance.portalContractAddress), + contractClass.publicFunctions.map(f => new EncodedContractFunction(f.selector, f.isInternal, f.bytecode)), + contractClass.id, + computeSaltedInitializationHash(instance), + instance.publicKeysHash, + ); } /** @@ -505,6 +570,10 @@ export class Archiver implements ArchiveSource { return this.store.getContractClass(id); } + public getContract(address: AztecAddress): Promise { + return this.store.getContractInstance(address); + } + /** * Gets up to `limit` amount of pending L1 to L2 messages. * @param limit - The number of messages to return. diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index f77bb26a27c..f55dd757bf4 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -48,7 +48,7 @@ import { SequencerClient, getGlobalVariableBuilder, } from '@aztec/sequencer-client'; -import { ContractClassPublic } from '@aztec/types/contracts'; +import { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/types/contracts'; import { MerkleTrees, ServerWorldStateSynchronizer, @@ -243,6 +243,10 @@ export class AztecNodeService implements AztecNode { return this.contractDataSource.getContractClass(id); } + public getContract(address: AztecAddress): Promise { + return this.contractDataSource.getContract(address); + } + /** * Gets up to `limit` amount of logs starting from `from`. * @param from - Number of the L2 block to which corresponds the first logs to be returned. diff --git a/yarn-project/circuit-types/src/contract_data.ts b/yarn-project/circuit-types/src/contract_data.ts index 9247a42d261..43cec7a3e6c 100644 --- a/yarn-project/circuit-types/src/contract_data.ts +++ b/yarn-project/circuit-types/src/contract_data.ts @@ -8,7 +8,7 @@ import { serializeBufferArrayToVector, serializeToBuffer, } from '@aztec/foundation/serialize'; -import { ContractClassPublic } from '@aztec/types/contracts'; +import { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/types/contracts'; /** * Used for retrieval of contract data (A3 address, portal contract address, bytecode). @@ -62,6 +62,12 @@ export interface ContractDataSource { * @param id - Contract class id. */ getContractClass(id: Fr): Promise; + + /** + * Returns a publicly deployed contract instance given its address. + * @param address - Address of the deployed contract. + */ + getContract(address: AztecAddress): Promise; } /** diff --git a/yarn-project/circuit-types/src/interfaces/aztec-node.ts b/yarn-project/circuit-types/src/interfaces/aztec-node.ts index 85809f4d5d2..10123d17b7d 100644 --- a/yarn-project/circuit-types/src/interfaces/aztec-node.ts +++ b/yarn-project/circuit-types/src/interfaces/aztec-node.ts @@ -10,7 +10,7 @@ import { import { L1ContractAddresses } from '@aztec/ethereum'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { Fr } from '@aztec/foundation/fields'; -import { ContractClassPublic } from '@aztec/types/contracts'; +import { ContractClassPublic, ContractInstanceWithAddress } from '@aztec/types/contracts'; import { ContractData, ExtendedContractData } from '../contract_data.js'; import { L1ToL2MessageAndIndex } from '../l1_to_l2_message.js'; @@ -286,4 +286,10 @@ export interface AztecNode { * @param id - Id of the contract class. */ getContractClass(id: Fr): Promise; + + /** + * Returns a publicly deployed contract instance given its address. + * @param address - Address of the deployed contract. + */ + getContract(address: AztecAddress): Promise; } diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index ecc7dadc0d3..27647730b03 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -61,6 +61,8 @@ export const REGISTERER_PRIVATE_FUNCTION_BROADCASTED_MAGIC_VALUE = 0x1b70e95fde0b70adc30496b90a327af6a5e383e028e7a43211a07bcdn; export const REGISTERER_UNCONSTRAINED_FUNCTION_BROADCASTED_MAGIC_VALUE = 0xe7af816635466f128568edb04c9fa024f6c87fb9010fdbffa68b3d99n; +export const DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE = + 0x85864497636cf755ae7bde03f267ce01a520981c21c3682aaf82a631n; export const L1_TO_L2_MESSAGE_LENGTH = 8; export const L1_TO_L2_MESSAGE_ORACLE_CALL_LENGTH = 25; export const MAX_NOTE_FIELDS_LENGTH = 20; diff --git a/yarn-project/circuits.js/src/contract/contract_class.ts b/yarn-project/circuits.js/src/contract/contract_class.ts index dd32d40588c..7238802fe36 100644 --- a/yarn-project/circuits.js/src/contract/contract_class.ts +++ b/yarn-project/circuits.js/src/contract/contract_class.ts @@ -13,7 +13,7 @@ type ContractArtifactWithHash = ContractArtifact & { artifactHash: Fr }; export function getContractClassFromArtifact( artifact: ContractArtifact | ContractArtifactWithHash, ): ContractClassWithId { - const artifactHash = (artifact as ContractArtifactWithHash).artifactHash ?? computeArtifactHash(artifact); + const artifactHash = 'artifactHash' in artifact ? artifact.artifactHash : computeArtifactHash(artifact); const publicFunctions: ContractClass['publicFunctions'] = artifact.functions .filter(f => f.functionType === FunctionType.OPEN) .map(f => ({ diff --git a/yarn-project/circuits.js/src/contract/contract_instance_deployed_event.ts b/yarn-project/circuits.js/src/contract/contract_instance_deployed_event.ts new file mode 100644 index 00000000000..134b24034b8 --- /dev/null +++ b/yarn-project/circuits.js/src/contract/contract_instance_deployed_event.ts @@ -0,0 +1,68 @@ +import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; +import { Fr } from '@aztec/foundation/fields'; +import { BufferReader } from '@aztec/foundation/serialize'; +import { ContractInstanceWithAddress } from '@aztec/types/contracts'; + +import { DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE } from '../constants.gen.js'; +import { AztecAddress, EthAddress } from '../index.js'; + +/** Event emitted from the ContractInstanceDeployer. */ +export class ContractInstanceDeployedEvent { + constructor( + public readonly address: AztecAddress, + public readonly version: number, + public readonly salt: Fr, + public readonly contractClassId: Fr, + public readonly initializationHash: Fr, + public readonly portalContractAddress: EthAddress, + public readonly publicKeysHash: Fr, + public readonly universalDeploy: boolean, + ) {} + + static isContractInstanceDeployedEvent(log: Buffer) { + return toBigIntBE(log.subarray(0, 32)) == DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE; + } + + static fromLogData(log: Buffer) { + if (!this.isContractInstanceDeployedEvent(log)) { + const magicValue = DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE.toString(16); + throw new Error(`Log data for ContractInstanceDeployedEvent is not prefixed with magic value 0x${magicValue}`); + } + const reader = new BufferReader(log.subarray(32)); + const address = reader.readObject(AztecAddress); + const version = reader.readObject(Fr).toNumber(); + const salt = reader.readObject(Fr); + const contractClassId = reader.readObject(Fr); + const initializationHash = reader.readObject(Fr); + const portalContractAddress = reader.readObject(EthAddress); + const publicKeysHash = reader.readObject(Fr); + const universalDeploy = reader.readObject(Fr).toBool(); + + return new ContractInstanceDeployedEvent( + address, + version, + salt, + contractClassId, + initializationHash, + portalContractAddress, + publicKeysHash, + universalDeploy, + ); + } + + toContractInstance(): ContractInstanceWithAddress { + if (this.version !== 1) { + throw new Error(`Unexpected contract instance version ${this.version}`); + } + + return { + address: this.address, + version: this.version, + contractClassId: this.contractClassId, + initializationHash: this.initializationHash, + portalContractAddress: this.portalContractAddress, + publicKeysHash: this.publicKeysHash, + salt: this.salt, + }; + } +} diff --git a/yarn-project/circuits.js/src/contract/index.ts b/yarn-project/circuits.js/src/contract/index.ts index 5ffe4c961b9..40ce0f0d8a2 100644 --- a/yarn-project/circuits.js/src/contract/index.ts +++ b/yarn-project/circuits.js/src/contract/index.ts @@ -7,3 +7,4 @@ export * from './contract_address.js'; export * from './private_function.js'; export * from './public_bytecode.js'; export * from './contract_class_registered_event.js'; +export * from './contract_instance_deployed_event.js'; diff --git a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts index 1918d32c195..a99395be793 100644 --- a/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts +++ b/yarn-project/end-to-end/src/cli_docs_sandbox.test.ts @@ -100,6 +100,7 @@ BenchmarkingContractArtifact CardGameContractArtifact ChildContractArtifact ContractClassRegistererContractArtifact +ContractInstanceDeployerContractArtifact CounterContractArtifact DocsExampleContractArtifact EasyPrivateTokenContractArtifact diff --git a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts index cddb33e1dec..86ec59834c1 100644 --- a/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_deploy_contract.test.ts @@ -8,6 +8,7 @@ import { ContractBase, ContractClassWithId, ContractDeployer, + ContractInstanceWithAddress, DebugLogger, EthAddress, Fr, @@ -24,6 +25,8 @@ import { ARTIFACT_FUNCTION_TREE_MAX_HEIGHT, MAX_PACKED_BYTECODE_SIZE_PER_PRIVATE_FUNCTION_IN_FIELDS, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS, + Point, + PublicKey, computeArtifactFunctionTree, computeArtifactFunctionTreeRoot, computeArtifactMetadataHash, @@ -31,11 +34,16 @@ import { computePrivateFunctionsRoot, computePrivateFunctionsTree, computePublicBytecodeCommitment, + computePublicKeysHash, } from '@aztec/circuits.js'; import { siloNullifier } from '@aztec/circuits.js/abis'; import { FunctionSelector, FunctionType, bufferAsFields } from '@aztec/foundation/abi'; import { padArrayEnd } from '@aztec/foundation/collection'; -import { ContractClassRegistererContract, ReaderContractArtifact, StatefulTestContract } from '@aztec/noir-contracts'; +import { + ContractClassRegistererContract, + ContractInstanceDeployerContract, + 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'; @@ -261,9 +269,10 @@ describe('e2e_deploy_contract', () => { expect(await contracts[1].methods.summed_values(owner).view()).toEqual(52n); }); - // Tests registering a new contract class on a node - // All this dance will be hidden behind a nicer API in the near future! - describe('registering a new contract class', () => { + // Tests registering a new contract class on a node and then deploying an instance. + // These tests look scary, but don't fret: all this hodgepodge of calls will be hidden + // behind a much nicer API in the near future as part of #4080. + describe('registering a contract class', () => { let registerer: ContractClassRegistererContract; let artifact: ContractArtifact; let contractClass: ContractClassWithId; @@ -272,11 +281,11 @@ describe('e2e_deploy_contract', () => { let publicBytecodeCommitment: Fr; beforeAll(async () => { - artifact = ReaderContractArtifact; + artifact = StatefulTestContract.artifact; contractClass = getContractClassFromArtifact(artifact); privateFunctionsRoot = computePrivateFunctionsRoot(contractClass.privateFunctions); publicBytecodeCommitment = computePublicBytecodeCommitment(contractClass.packedBytecode); - registerer = await registerContract(wallet, ContractClassRegistererContract, [], new Fr(1)); + registerer = await registerContract(wallet, ContractClassRegistererContract, [], { salt: new Fr(1) }); logger(`contractClass.id: ${contractClass.id}`); logger(`contractClass.artifactHash: ${contractClass.artifactHash}`); @@ -324,6 +333,10 @@ describe('e2e_deploy_contract', () => { // since it's common to provide all hash preimages to a function that verifies them. const artifactMetadataHash = computeArtifactMetadataHash(artifact); const unconstrainedArtifactFunctionTreeRoot = computeArtifactFunctionTreeRoot(artifact, FunctionType.OPEN); + + // We need two sibling paths because private function information is split across two trees: + // The "private function tree" captures the selectors and verification keys, and is used in the kernel circuit for verifying the proof generated by the app circuit. + // The "artifact tree" captures function bytecode and metadata, and is used by the pxe to check that its executing the code it's supposed to be executing, but it never goes into circuits. const privateFunctionTreePath = computePrivateFunctionsTree(contractClass.privateFunctions).getSiblingPath(0); const artifactFunctionTreePath = computeArtifactFunctionTree(artifact, FunctionType.SECRET)!.getSiblingPath(0); @@ -376,6 +389,71 @@ describe('e2e_deploy_contract', () => { .send() .wait(); }, 60_000); + + describe('deploying a contract instance', () => { + let instance: ContractInstanceWithAddress; + let deployer: ContractInstanceDeployerContract; + let deployTxHash: TxHash; + let initArgs: StatefulContractCtorArgs; + let publicKey: PublicKey; + + beforeAll(async () => { + initArgs = [accounts[0].address, 42]; + deployer = await registerContract(wallet, ContractInstanceDeployerContract, [], { salt: new Fr(1) }); + + const salt = Fr.random(); + const portalAddress = EthAddress.random(); + publicKey = Point.random(); + const publicKeysHash = computePublicKeysHash(publicKey); + + instance = getContractInstanceFromDeployParams(artifact, initArgs, salt, publicKey, portalAddress); + const tx = await deployer.methods + .deploy(salt, contractClass.id, instance.initializationHash, portalAddress, publicKeysHash, false) + .send() + .wait(); + deployTxHash = tx.txHash; + }); + + it('emits deployment log', async () => { + const logs = await pxe.getUnencryptedLogs({ txHash: deployTxHash }); + const deployedLog = logs.logs[0].log; + expect(deployedLog.contractAddress).toEqual(deployer.address); + }); + + it('stores contract instance in the aztec node', async () => { + const deployed = await aztecNode.getContract(instance.address); + expect(deployed).toBeDefined(); + expect(deployed!.address).toEqual(instance.address); + expect(deployed!.contractClassId).toEqual(contractClass.id); + expect(deployed!.initializationHash).toEqual(instance.initializationHash); + expect(deployed!.portalContractAddress).toEqual(instance.portalContractAddress); + expect(deployed!.publicKeysHash).toEqual(instance.publicKeysHash); + expect(deployed!.salt).toEqual(instance.salt); + }); + + it('calls a public function on the deployed instance', async () => { + // TODO(@spalladino) We should **not** need the whole instance, including initArgs and salt, + // in order to interact with a public function for the contract. We may even not need + // all of it for running a private function. Consider removing `instance` as a required + // field in the aztec.js `Contract` class, maybe we can replace it with just the partialAddress. + // Not just that, but this instance has been broadcasted, so the pxe should be able to get + // its information from the node directly, excluding private functions, but it's ok because + // we are not going to run those - but this may require registering "partial" contracts in the pxe. + // Anyway, when we implement that, we should be able to replace this `registerContract` with + // a simpler `Contract.at(instance.address, wallet)`. + const registered = await registerContract(wallet, StatefulTestContract, initArgs, { + salt: instance.salt, + portalAddress: instance.portalContractAddress, + publicKey, + }); + expect(registered.address).toEqual(instance.address); + const contract = await StatefulTestContract.at(instance.address, wallet); + const whom = AztecAddress.random(); + await contract.methods.increment_public_value(whom, 10).send({ skipPublicSimulation: true }).wait(); + const stored = await contract.methods.get_public_value(whom).view(); + expect(stored).toEqual(10n); + }); + }); }); }); @@ -396,9 +474,10 @@ async function registerContract( wallet: Wallet, contractArtifact: ContractArtifactClass, args: any[] = [], - salt?: Fr, + opts: { salt?: Fr; publicKey?: Point; portalAddress?: EthAddress } = {}, ): Promise { - const instance = getContractInstanceFromDeployParams(contractArtifact.artifact, args, salt); + const { salt, publicKey, portalAddress } = opts; + const instance = getContractInstanceFromDeployParams(contractArtifact.artifact, args, salt, publicKey, portalAddress); await wallet.addContracts([{ artifact: contractArtifact.artifact, instance }]); return contractArtifact.at(instance.address, wallet); } diff --git a/yarn-project/foundation/src/abi/buffer.ts b/yarn-project/foundation/src/abi/buffer.ts index b4c45af4c46..501f8070ea7 100644 --- a/yarn-project/foundation/src/abi/buffer.ts +++ b/yarn-project/foundation/src/abi/buffer.ts @@ -13,7 +13,7 @@ export function bufferAsFields(input: Buffer, targetLength: number): Fr[] { const encoded = [ new Fr(input.length), ...chunk(input, Fr.SIZE_IN_BYTES - 1).map(c => { - const fieldBytes = Buffer.alloc(32); + const fieldBytes = Buffer.alloc(Fr.SIZE_IN_BYTES); Buffer.from(c).copy(fieldBytes, 1); return Fr.fromBuffer(fieldBytes); }), diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index 0aebb8fa3cc..28e0a64dad8 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -95,6 +95,10 @@ abstract class BaseField { return this.asBigInt; } + toBool(): boolean { + return Boolean(this.toBigInt()); + } + toNumber(): number { const value = this.toBigInt(); if (value > Number.MAX_SAFE_INTEGER) { diff --git a/yarn-project/noir-contracts/Nargo.toml b/yarn-project/noir-contracts/Nargo.toml index d5db8034622..d55caac75d5 100644 --- a/yarn-project/noir-contracts/Nargo.toml +++ b/yarn-project/noir-contracts/Nargo.toml @@ -5,6 +5,7 @@ members = [ "contracts/card_game_contract", "contracts/child_contract", "contracts/contract_class_registerer_contract", + "contracts/contract_instance_deployer_contract", "contracts/counter_contract", "contracts/docs_example_contract", "contracts/easy_private_token_contract", diff --git a/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/Nargo.toml b/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/Nargo.toml new file mode 100644 index 00000000000..aeca13c16a4 --- /dev/null +++ b/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "contract_instance_deployer_contract" +authors = [""] +compiler_version = ">=0.18.0" +type = "contract" + +[dependencies] +aztec = { path = "../../../aztec-nr/aztec" } diff --git a/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/events.nr b/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/events.nr new file mode 100644 index 00000000000..d2b6ed6033f --- /dev/null +++ b/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/events.nr @@ -0,0 +1 @@ +mod instance_deployed; diff --git a/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/events/instance_deployed.nr b/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/events/instance_deployed.nr new file mode 100644 index 00000000000..932973d45b1 --- /dev/null +++ b/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/events/instance_deployed.nr @@ -0,0 +1,36 @@ +use dep::aztec::protocol_types::{ + contract_class::ContractClassId, + address::{ AztecAddress, EthAddress, PublicKeysHash, PartialAddress }, + constants::{DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE}, + traits::{Serialize} +}; + +// #[event] +struct ContractInstanceDeployed { + address: AztecAddress, + version: u8, + salt: Field, + contract_class_id: ContractClassId, + initialization_hash: Field, + portal_contract_address: EthAddress, + public_keys_hash: PublicKeysHash, + universal_deploy: bool, +} + +global CONTRACT_INSTANCE_DEPLOYED_SERIALIZED_SIZE: Field = 9; + +impl Serialize for ContractInstanceDeployed { + fn serialize(self: Self) -> [Field; CONTRACT_INSTANCE_DEPLOYED_SERIALIZED_SIZE] { + [ + DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE, + self.address.to_field(), + self.version as Field, + self.salt, + self.contract_class_id.to_field(), + self.initialization_hash, + self.portal_contract_address.to_field(), + self.public_keys_hash.to_field(), + self.universal_deploy as Field, + ] + } +} diff --git a/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/main.nr b/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/main.nr new file mode 100644 index 00000000000..679085f994c --- /dev/null +++ b/yarn-project/noir-contracts/contracts/contract_instance_deployer_contract/src/main.nr @@ -0,0 +1,63 @@ +mod events; + +contract ContractInstanceDeployer { + use dep::std::option::Option; + use dep::aztec::protocol_types::{ + address::{ AztecAddress, EthAddress, PublicKeysHash, PartialAddress }, + contract_class::ContractClassId, + constants::{DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE}, + traits::{Serialize} + }; + + use dep::aztec::log::{ emit_unencrypted_log, emit_unencrypted_log_from_private}; + + use crate::events::{ + instance_deployed::ContractInstanceDeployed, + }; + + #[aztec(private)] + fn constructor() {} + + #[aztec(private)] + fn deploy( + salt: Field, + contract_class_id: ContractClassId, + initialization_hash: Field, + portal_contract_address: EthAddress, + public_keys_hash: PublicKeysHash, + universal_deploy: bool + ) { + // TODO(@spalladino): assert nullifier_exists silo(contract_class_id, ContractClassRegisterer) + // TODO(@spalladino): assert is_valid_eth_address(portal_contract_address) + + // TODO(#4434) Add deployer field to instance calculation + // let deployer = if universal_deploy { Field::zero() } else { context.msg_sender() }; + + let partial_address = PartialAddress::compute( + contract_class_id, + salt, + initialization_hash, + portal_contract_address + ); + + let address = AztecAddress::compute(public_keys_hash, partial_address); + + // Emit the address as a nullifier to be able to prove that this instance has been (not) deployed + context.push_new_nullifier(address.to_field(), 0); + + // Broadcast the event + let event = ContractInstanceDeployed { + contract_class_id, + address, + public_keys_hash, + portal_contract_address, + initialization_hash, + salt, + universal_deploy, + version: 1 + }; + let event_payload = event.serialize(); + dep::aztec::oracle::debug_log::debug_log_array_with_prefix("ContractInstanceDeployed", event_payload); + emit_unencrypted_log_from_private(&mut context, event_payload); + } +} diff --git a/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr b/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr index ca90157034c..0b40f7ad4b3 100644 --- a/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr +++ b/yarn-project/noir-contracts/contracts/stateful_test_contract/src/main.nr @@ -49,6 +49,12 @@ contract StatefulTest { increment(recipient_notes, amount, recipient); } + #[aztec(public)] + fn increment_public_value(owner: AztecAddress, value: Field) { + let loc = storage.public_values.at(owner); + loc.write(loc.read() + value); + } + unconstrained fn summed_values(owner: AztecAddress) -> pub Field { let owner_balance = storage.notes.at(owner); @@ -56,6 +62,10 @@ contract StatefulTest { balance_utils::get_balance(owner_balance) } + unconstrained fn get_public_value(owner: AztecAddress) -> pub Field { + storage.public_values.at(owner).read() + } + unconstrained fn compute_note_hash_and_nullifier( contract_address: AztecAddress, nonce: Field, diff --git a/yarn-project/noir-protocol-circuits/src/crates/types/src/address.nr b/yarn-project/noir-protocol-circuits/src/crates/types/src/address.nr index 33e56273756..d4dc79fd9bf 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/types/src/address.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/types/src/address.nr @@ -248,6 +248,18 @@ impl ToField for PublicKeysHash { } } +impl Serialize<1> for PublicKeysHash { + fn serialize(self: Self) -> [Field; 1] { + [self.to_field()] + } +} + +impl Deserialize<1> for PublicKeysHash { + fn deserialize(fields: [Field; 1]) -> Self { + PublicKeysHash::from_field(fields[0]) + } +} + impl PublicKeysHash { pub fn from_field(field : Field) -> Self { Self { diff --git a/yarn-project/noir-protocol-circuits/src/crates/types/src/constants.nr b/yarn-project/noir-protocol-circuits/src/crates/types/src/constants.nr index c9483669b1c..52d787e528c 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/types/src/constants.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/types/src/constants.nr @@ -107,6 +107,10 @@ global REGISTERER_PRIVATE_FUNCTION_BROADCASTED_MAGIC_VALUE = 0x1b70e95fde0b70adc // sha224sum 'struct ClassUnconstrainedFunctionBroadcasted' global REGISTERER_UNCONSTRAINED_FUNCTION_BROADCASTED_MAGIC_VALUE = 0xe7af816635466f128568edb04c9fa024f6c87fb9010fdbffa68b3d99; +// CONTRACT INSTANCE CONSTANTS +// sha224sum 'struct ContractInstanceDeployed' +global DEPLOYER_CONTRACT_INSTANCE_DEPLOYED_MAGIC_VALUE = 0x85864497636cf755ae7bde03f267ce01a520981c21c3682aaf82a631; + // NOIR CONSTANTS - constants used only in yarn-packages/noir-contracts // Some are defined here because Noir doesn't yet support globals referencing other globals yet. // Move these constants to a noir file once the issue below is resolved: