diff --git a/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr b/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr index 01e776434ae2..7066bfbb7a55 100644 --- a/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr +++ b/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr @@ -1,12 +1,18 @@ use dep::protocol_types::{ - abis::new_contract_data::NewContractData as ContractLeafPreimage, + abis::{ + complete_address::CompleteAddress, + new_contract_data::NewContractData as ContractLeafPreimage, + }, address::{AztecAddress, EthAddress}, + point::Point, + hash::compute_partial_address, }; use dep::std::merkle::compute_merkle_root; use crate::{ context::PrivateContext, oracle::get_membership_witness::get_contract_membership_witness, + oracle::debug_log::debug_log_format, }; // Proves that the contract address, portal address and function tree root form a valid contract preimage of a leaf @@ -14,22 +20,44 @@ use crate::{ // Note: This can be used to approximate a factory pattern --> a factory contract could perform this proof and that // way verify that a contract at a given address is what it expects. Then it could store it in an internal // map of contracts (like what Uniswap Factory does with pool contracts - it stores them in a mapping). +// By passing in the construct hash the factory can also verify that the contract was constructed with the +// correct constructor arguments. Typically the factory would store the expect construct hash and assert that +// it is what it expects. pub fn prove_contract_inclusion( - contract_address: AztecAddress, - portal_contract_address: EthAddress, + deployer_public_key: Point, + contract_address_salt: Field, function_tree_root: Field, - block_number: u32, // The block at which we'll prove that the contract exists + constructor_hash: Field, + portal_contract_address: EthAddress, + block_number: u32, // The block at which we'll prove that the public value exists context: PrivateContext -) { +) -> AztecAddress { // 1) Get block header from oracle and ensure that the block is included in the archive. let block_header = context.get_block_header(block_number); - // 2) Form the contract tree leaf preimage - let preimage = ContractLeafPreimage { - contract_address, - portal_contract_address, + let partial_address = compute_partial_address(contract_address_salt, function_tree_root, constructor_hash); + debug_log_format("partial_address: {0}", [partial_address]); + + // 2) Compute the contract address + let complete_contract_address = CompleteAddress::compute( + deployer_public_key, + contract_address_salt, function_tree_root, - }; + constructor_hash + ); + + let contract_address = complete_contract_address.address; + + debug_log_format( + "deployer_pubKey .x: {0} .y: {1}, salt: {2}, function_tree_root: {3}, constructor_hash: {4}, contract address: {5}, partial_address: {6}", + [ + deployer_public_key.x, deployer_public_key.y, contract_address_salt, function_tree_root, constructor_hash, contract_address.to_field(), + complete_contract_address.partial_address + ] + ); + + // 2) Form the contract tree leaf preimage + let preimage = ContractLeafPreimage { contract_address, portal_contract_address, function_tree_root }; // 3) Get the contract tree leaf by hashing the preimage let contract_leaf = preimage.hash(); @@ -39,9 +67,11 @@ pub fn prove_contract_inclusion( // 5) Prove that the leaf is in the contract tree assert( - block_header.contract_tree_root == compute_merkle_root(contract_leaf, witness.index, witness.path), - "Proving contract inclusion failed" + block_header.contract_tree_root + == compute_merkle_root(contract_leaf, witness.index, witness.path), "Proving contract inclusion failed" ); // --> Now we have traversed the trees all the way up to archive root. -} \ No newline at end of file + + contract_address +} diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 4bfe7a298b79..bf7610adc414 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -42,7 +42,6 @@ export { isContractDeployed, EthCheatCodes, computeAuthWitMessageHash, - computeContractFunctionTreeRoot, } from './utils/index.js'; export { createPXEClient } from './pxe_client.js'; diff --git a/yarn-project/aztec.js/src/utils/l2_contracts.ts b/yarn-project/aztec.js/src/utils/l2_contracts.ts index 6f75e16fea0c..67fcdb2851b2 100644 --- a/yarn-project/aztec.js/src/utils/l2_contracts.ts +++ b/yarn-project/aztec.js/src/utils/l2_contracts.ts @@ -1,6 +1,3 @@ -import { Fr, generateFunctionLeaves } from '@aztec/circuits.js'; -import { computeFunctionTreeRoot } from '@aztec/circuits.js/abis'; -import { ContractArtifact, FunctionSelector } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { PXE } from '@aztec/types'; @@ -13,17 +10,3 @@ import { PXE } from '@aztec/types'; export async function isContractDeployed(pxe: PXE, contractAddress: AztecAddress): Promise { return !!(await pxe.getContractData(contractAddress)); } - -/** - * Computes the root of a function tree for a given smart contract artifact. - * @param artifact - The smart contract artifact. - * @returns The computed function tree root based on the functions in the given contract artifact. - */ -export function computeContractFunctionTreeRoot(artifact: ContractArtifact): Fr { - const functions = artifact.functions.map(f => ({ - ...f, - selector: FunctionSelector.fromNameAndParameters(f.name, f.parameters), - })); - const functionLeaves = generateFunctionLeaves(functions); - return computeFunctionTreeRoot(functionLeaves); -} diff --git a/yarn-project/circuits.js/src/abis/abis.ts b/yarn-project/circuits.js/src/abis/abis.ts index 6d268d655185..f86152bc635d 100644 --- a/yarn-project/circuits.js/src/abis/abis.ts +++ b/yarn-project/circuits.js/src/abis/abis.ts @@ -182,6 +182,9 @@ export function computeCompleteAddress( constructorHash: Fr, ): CompleteAddress { const partialAddress = computePartialAddress(contractAddrSalt, fnTreeRoot, constructorHash); + // console.log( + // `salt: ${contractAddrSalt.toString()}, function_tree_root: ${fnTreeRoot.toString()}, constructor_hash: ${constructorHash.toString()}, partial_address ${partialAddress.toString()}`, + // ); return new CompleteAddress( computeContractAddressFromPartial(deployerPubKey, partialAddress), deployerPubKey, @@ -192,7 +195,7 @@ export function computeCompleteAddress( /** * */ -function computePartialAddress(contractAddrSalt: Fr, fnTreeRoot: Fr, constructorHash: Fr) { +export function computePartialAddress(contractAddrSalt: Fr, fnTreeRoot: Fr, constructorHash: Fr) { return Fr.fromBuffer( pedersenHash( [ diff --git a/yarn-project/circuits.js/src/contract/contract_deployment_info.ts b/yarn-project/circuits.js/src/contract/contract_deployment_info.ts index 38a15d1cfcc2..fecb5a057a2e 100644 --- a/yarn-project/circuits.js/src/contract/contract_deployment_info.ts +++ b/yarn-project/circuits.js/src/contract/contract_deployment_info.ts @@ -48,7 +48,7 @@ export function getContractDeploymentInfo( return { completeAddress, - constructorHash: constructorVkHash, + constructorHash: constructorVkHash, // !!!!!!!!!!!!! functionTreeRoot, }; } diff --git a/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts b/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts index 55a05381182b..452df0c5c0a6 100644 --- a/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts @@ -5,8 +5,10 @@ import { Fr, INITIAL_L2_BLOCK_NUM, PXE, - computeContractFunctionTreeRoot, + Point, + getContractDeploymentInfo, } from '@aztec/aztec.js'; +import { computePartialAddress } from '@aztec/circuits.js/abis'; import { InclusionProofsContract } from '@aztec/noir-contracts/types'; import { jest } from '@jest/globals'; @@ -29,12 +31,12 @@ describe('e2e_inclusion_proofs_contract', () => { let contract: InclusionProofsContract; let deploymentBlockNumber: number; const publicValue = 236n; - let contractFunctionTreeRoot: Fr; + const contractAddressSalt = Fr.random(); beforeAll(async () => { ({ pxe, teardown, wallets, accounts } = await setup(1)); - const receipt = await InclusionProofsContract.deploy(wallets[0], publicValue).send().wait(); + const receipt = await InclusionProofsContract.deploy(wallets[0], publicValue).send({ contractAddressSalt }).wait(); contract = receipt.contract; deploymentBlockNumber = receipt.blockNumber!; }, 100_000); @@ -168,35 +170,70 @@ describe('e2e_inclusion_proofs_contract', () => { ).rejects.toThrow(/Low nullifier witness not found for nullifier 0x[0-9a-fA-F]+ at block/); }); - it('proves existence of a contract', async () => { + it.only('proves existence of a contract', async () => { // Choose random block number between first block and current block number to test archival node const blockNumber = await getRandomBlockNumberSinceDeployment(); - const contractAddress = contract.address; + const contractArtifact = contract.artifact; + + // bellow are contents of a preimage of AztecAddress + const constructorArgs = [publicValue]; + const publicKey = Point.ZERO; // InclusionProofs contract doesn't have associated public key because it's not an account contract + + console.log("+++++++++++++++++++++++++++++++++++++") + const deploymentData = getContractDeploymentInfo(contractArtifact, constructorArgs, contractAddressSalt, publicKey); + + // console.log( + // `deployer_pubKey .x: ${publicKey.x.toString()} .y: ${publicKey.y.toString()}, salt: ${contractAddressSalt.toString()}, function_tree_root: ${deploymentData.functionTreeRoot.toString()}, constructor_hash: ${deploymentData.constructorHash.toString()}, contract address: ${contract.address.toString()}, partial_address ${deploymentData.completeAddress.partialAddress.toString()}`, + // ); + + const constructorHash = deploymentData.constructorHash; + const portalContractAddress = contract.portalContract; - const functionTreeRoot = getContractFunctionTreeRoot(); + const functionTreeRoot = deploymentData.functionTreeRoot; + + console.log("computePartialAddress, constructorHash", constructorHash.toString()); + const partialAddress = computePartialAddress(contractAddressSalt, functionTreeRoot, constructorHash); + + // console.log(`salt: ${contractAddressSalt.toString()}, function_tree_root: ${deploymentData.functionTreeRoot.toString()}, constructor_hash: ${constructorHash.toString()}, partial_address: ${partialAddress.toString()}`); + + if (!partialAddress.equals(deploymentData.completeAddress.partialAddress)) { + throw new Error('Partial address mismatch'); + } await contract.methods - .test_contract_inclusion_proof(contractAddress, portalContractAddress, functionTreeRoot, blockNumber) + .test_contract_inclusion_proof( + publicKey, + contractAddressSalt, + functionTreeRoot, + constructorHash, + portalContractAddress, + blockNumber, + ) .send() .wait(); }); - it('contract existence failure case', async () => { - // This should fail because we choose a block number before the contract was deployed - const blockNumber = deploymentBlockNumber - 1; - - const contractAddress = contract.address; - const portalContractAddress = contract.portalContract; - const functionTreeRoot = getContractFunctionTreeRoot(); - - await expect( - contract.methods - .test_contract_inclusion_proof(contractAddress, portalContractAddress, functionTreeRoot, blockNumber) - .send() - .wait(), - ).rejects.toThrow(/Leaf value: 0x[0-9a-fA-F]+ not found in CONTRACT_TREE/); - }); + // it('contract existence failure case', async () => { + // // This should fail because we choose a block number before the contract was deployed + // const blockNumber = deploymentBlockNumber - 1; + + // const contractAddress = contract.address; // I will unwind contract address + // // I need to pass in: + // // 1) deployer pub key, + // // 2) contract address salt, + // // 3) function tree root, DONE + // // 4) constructor hash + // const portalContractAddress = contract.portalContract; + // const functionTreeRoot = getContractFunctionTreeRoot(); + + // await expect( + // contract.methods + // .test_contract_inclusion_proof(contractAddress, portalContractAddress, functionTreeRoot, blockNumber) + // .send() + // .wait(), + // ).rejects.toThrow(/Leaf value: 0x[0-9a-fA-F]+ not found in CONTRACT_TREE/); + // }); const getRandomBlockNumberSinceDeployment = async () => { const currentBlockNumber = await pxe.getBlockNumber(); @@ -207,11 +244,4 @@ describe('e2e_inclusion_proofs_contract', () => { const currentBlockNumber = await pxe.getBlockNumber(); return deploymentBlockNumber + Math.floor(Math.random() * (currentBlockNumber - INITIAL_L2_BLOCK_NUM)); }; - - const getContractFunctionTreeRoot = () => { - if (!contractFunctionTreeRoot) { - contractFunctionTreeRoot = computeContractFunctionTreeRoot(contract.artifact); - } - return contractFunctionTreeRoot; - }; }); diff --git a/yarn-project/noir-contracts/src/contracts/inclusion_proofs_contract/src/main.nr b/yarn-project/noir-contracts/src/contracts/inclusion_proofs_contract/src/main.nr index 23189ba86612..5b1738d79b56 100644 --- a/yarn-project/noir-contracts/src/contracts/inclusion_proofs_contract/src/main.nr +++ b/yarn-project/noir-contracts/src/contracts/inclusion_proofs_contract/src/main.nr @@ -1,8 +1,11 @@ // A demonstration of inclusion and non-inclusion proofs. contract InclusionProofs { - use dep::protocol_types::address::{ - AztecAddress, - EthAddress, + use dep::protocol_types::{ + address::{ + AztecAddress, + EthAddress, + }, + point::Point, }; use dep::aztec::{ state_vars::{ @@ -83,22 +86,18 @@ contract InclusionProofs { // Creates a value note owned by `owner`. #[aztec(private)] - fn create_note( - owner: AztecAddress, - value: Field, - ) { + fn create_note(owner: AztecAddress, value: Field) { let owner_private_values = storage.private_values.at(owner.to_field()); let mut note = ValueNote::new(value, owner); owner_private_values.insert(&mut note, true); } - // Proves that the owner owned a ValueNote at block `block_number`. #[aztec(private)] fn test_note_inclusion_proof( owner: AztecAddress, block_number: u32, // The block at which we'll prove that the note exists - spare_commitment: Field, // This is only used when the note is not found --> used to test the failure case + spare_commitment: Field // This is only used when the note is not found --> used to test the failure case ) { // 1) Get the note from PXE. let private_values = storage.private_values.at(owner.to_field()); @@ -108,7 +107,12 @@ contract InclusionProofs { // 2) Prove the note inclusion if maybe_note.is_some() { - prove_note_inclusion(ValueNoteMethods, maybe_note.unwrap_unchecked(), block_number, context); + prove_note_inclusion( + ValueNoteMethods, + maybe_note.unwrap_unchecked(), + block_number, + context + ); } else { // Note was not found so we will prove inclusion of the spare commitment prove_note_commitment_inclusion(spare_commitment, block_number, context); @@ -120,7 +124,7 @@ contract InclusionProofs { fn test_nullifier_non_inclusion_proof( owner: AztecAddress, block_number: u32, // The block at which we'll prove that the nullifier does not exists - spare_nullifier: Field, // This is only used when the note is not found --> used to test the failure case + spare_nullifier: Field // This is only used when the note is not found --> used to test the failure case ) { // 2) Get the note from PXE let private_values = storage.private_values.at(owner.to_field()); @@ -130,7 +134,12 @@ contract InclusionProofs { // 3) Compute the nullifier from the note if maybe_note.is_some() { - prove_note_not_nullified(ValueNoteMethods, maybe_note.unwrap_unchecked(), block_number, context); + prove_note_not_nullified( + ValueNoteMethods, + maybe_note.unwrap_unchecked(), + block_number, + context + ); } else { // Note was not found so we will use the spare nullifier prove_nullifier_non_inclusion(spare_nullifier, block_number, context); @@ -140,7 +149,7 @@ contract InclusionProofs { #[aztec(private)] fn test_note_validity_proof( owner: AztecAddress, - block_number: u32, // The block at which we'll prove that the note exists and is not nullified + block_number: u32 // The block at which we'll prove that the note exists and is not nullified ) { // 1) Get the note from PXE. let private_values = storage.private_values.at(owner.to_field()); @@ -153,9 +162,7 @@ contract InclusionProofs { } #[aztec(private)] - fn nullify_note( - owner: AztecAddress, - ) { + fn nullify_note(owner: AztecAddress) { let private_values = storage.private_values.at(owner.to_field()); let options = NoteGetterOptions::new().select(1, owner.to_field()).set_limit(1); let notes = private_values.get_notes(options); @@ -170,7 +177,7 @@ contract InclusionProofs { #[aztec(private)] fn test_nullifier_inclusion_proof( nullifier: Field, - block_number: u32, // The block at which we'll prove that the nullifier not exists in the tree + block_number: u32 // The block at which we'll prove that the nullifier not exists in the tree ) { prove_nullifier_inclusion(nullifier, block_number, context); } @@ -178,9 +185,14 @@ contract InclusionProofs { #[aztec(private)] fn test_public_value_inclusion_proof( public_value: Field, - block_number: u32, // The block at which we'll prove that the public value exists + block_number: u32 // The block at which we'll prove that the public value exists ) { - prove_public_value_inclusion(public_value, storage.public_value.storage_slot, block_number, context); + prove_public_value_inclusion( + public_value, + storage.public_value.storage_slot, + block_number, + context + ); } // Proves that the contract address, portal address and function tree root form a valid contract preimage of a leaf @@ -188,22 +200,41 @@ contract InclusionProofs { // Note: This can be used to approximate a factory pattern --> a factory contract could perform this proof and that // way verify that a contract at a given address is what it expects. Then it could store it in an internal // map of contracts (like what Uniswap Factory does with pool contracts - it stores them in a mapping). + // By passing in the construct hash the factory can also verify that the contract was constructed with the + // correct constructor arguments. Typically the factory would store the expect construct hash and assert that + // it is what it expects. #[aztec(private)] fn test_contract_inclusion_proof( - contract_address: AztecAddress, - portal_contract_address: EthAddress, + deployer_public_key: Point, + contract_address_salt: Field, function_tree_root: Field, - block_number: u32, // The block at which we'll prove that the public value exists + constructor_hash: Field, + portal_contract_address: EthAddress, + block_number: u32 // The block at which we'll prove that the public value exists ) { - prove_contract_inclusion(contract_address, portal_contract_address, function_tree_root, block_number, context); + let proven_contract_address = prove_contract_inclusion( + deployer_public_key, + contract_address_salt, + function_tree_root, + constructor_hash, + portal_contract_address, + block_number, + context + ); + // Here typically the factory would add the contract address to its internal map of deployed contracts. } // Computes note hash and nullifier. // Note 1: Needs to be defined by every contract producing logs. // Note 2: Having it in all the contracts gives us the ability to compute the note hash and nullifier differently for different kind of notes. - unconstrained fn compute_note_hash_and_nullifier(contract_address: Field, nonce: Field, storage_slot: Field, serialized_note: [Field; VALUE_NOTE_LEN]) -> [Field; 4] { + unconstrained fn compute_note_hash_and_nullifier( + contract_address: Field, + nonce: Field, + storage_slot: Field, + serialized_note: [Field; VALUE_NOTE_LEN] + ) -> [Field; 4] { let _address = AztecAddress::from_field(contract_address); // TODO(benesjan) https://github.com/AztecProtocol/aztec-packages/issues/3669 let note_header = NoteHeader::new(_address, nonce, storage_slot); note_utils::compute_note_hash_and_nullifier(ValueNoteMethods, note_header, serialized_note) } -} \ No newline at end of file +} diff --git a/yarn-project/noir-protocol-circuits/src/crates/types/src/point.nr b/yarn-project/noir-protocol-circuits/src/crates/types/src/point.nr index 6482295491e1..1f6b5da720e1 100644 --- a/yarn-project/noir-protocol-circuits/src/crates/types/src/point.nr +++ b/yarn-project/noir-protocol-circuits/src/crates/types/src/point.nr @@ -4,6 +4,8 @@ struct Point { y: Field, } +global POINT_SERIALIZED_LEN: Field = 2; + impl Point { pub fn zero() -> Self { Point { @@ -12,6 +14,10 @@ impl Point { } } + fn serialize(self) -> [Field; POINT_SERIALIZED_LEN] { + [self.x, self.y] + } + // TODO(David): Would be quite careful here as (0,0) is not a point // on the curve. A boolean flag may be the better approach here, // would also cost less constraints. It seems like we don't need to