From b378931909162a7e7bfdb6add170976fad6dc488 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 13 Dec 2023 14:47:22 +0000 Subject: [PATCH 01/12] WIP --- .../aztec/src/history/contract_inclusion.nr | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr diff --git a/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr b/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr new file mode 100644 index 00000000000..0c474e1fc97 --- /dev/null +++ b/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr @@ -0,0 +1,48 @@ +use dep::protocol_types::{ + abis::new_contract_data::NewContractData as ContractLeafPreimage, + constants::CONTRACT_TREE_HEIGHT, +}; +use dep::std::merkle::compute_merkle_root; +use crate::abis::new_contract_data::NewContractData as ContractLeafPreimage; + +use crate::{ + context::PrivateContext, + oracle::get_membership_witness::{ + get_membership_witness, + MembershipWitness, + }, +}; + +pub fn prove_contract_inclusion( + contract_address: Address, + portal_contract_address: EthAddress, + function_tree_root: Field, + block_number: u32, // The block at which we'll prove that the note exists + context: PrivateContext +) { + // 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 + leaf_preimage = ContractLeafPreimage { + contract_address, + portal_contract_address, + function_tree_root, + }; + + // 3) Get the contract tree leaf by hashing the preimage + let leaf_preimage = preimage.hash(); + + // 4) Get the membership witness of the leaf in the contract tree + let contract_tree_id = 2; // TODO(#3443) + let witness: MembershipWitness = + get_membership_witness(block_number, contract_tree_id, contract_leaf); + + // 3) 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 note inclusion failed" + ); + + // --> Now we have traversed the trees all the way up to archive root. +} \ No newline at end of file From 4bf6b278fbe7aa2e4dc6f93f561a7cc22028bb74 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 13 Dec 2023 15:19:08 +0000 Subject: [PATCH 02/12] WIP --- yarn-project/aztec-nr/aztec/src/history.nr | 1 + .../aztec/src/history/contract_inclusion.nr | 7 ++++--- .../inclusion_proofs_contract/src/main.nr | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/yarn-project/aztec-nr/aztec/src/history.nr b/yarn-project/aztec-nr/aztec/src/history.nr index c36332d365a..597196eba97 100644 --- a/yarn-project/aztec-nr/aztec/src/history.nr +++ b/yarn-project/aztec-nr/aztec/src/history.nr @@ -1,3 +1,4 @@ +mod contract_inclusion; mod note_inclusion; mod note_validity; mod nullifier_inclusion; 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 0c474e1fc97..230744ce796 100644 --- a/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr +++ b/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr @@ -1,5 +1,6 @@ use dep::protocol_types::{ abis::new_contract_data::NewContractData as ContractLeafPreimage, + address::{Address, EthAddress}, constants::CONTRACT_TREE_HEIGHT, }; use dep::std::merkle::compute_merkle_root; @@ -17,7 +18,7 @@ pub fn prove_contract_inclusion( contract_address: Address, portal_contract_address: EthAddress, function_tree_root: Field, - block_number: u32, // The block at which we'll prove that the note exists + block_number: u32, // The block at which we'll prove that the contract exists context: PrivateContext ) { // 1) Get block header from oracle and ensure that the block is included in the archive. @@ -38,10 +39,10 @@ pub fn prove_contract_inclusion( let witness: MembershipWitness = get_membership_witness(block_number, contract_tree_id, contract_leaf); - // 3) Prove that the leaf is in the contract tree + // 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 note inclusion failed" + "Proving contract inclusion failed" ); // --> Now we have traversed the trees all the way up to archive root. 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 c0516b90468..2bfabb6ea65 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 @@ -19,6 +19,9 @@ contract InclusionProofs { }, history::{ + contract_inclusion::{ + prove_contract_inclusion, + }, note_inclusion::{ prove_note_commitment_inclusion, prove_note_inclusion, @@ -177,6 +180,21 @@ contract InclusionProofs { 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 + // which exists at block `block_number`. + // 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 in an internal map + // of contracts (similarily to what Uniswap Factory does with pool contracts). + #[aztec(private)] + fn test_contract_inclusion_proof( + contract_address: Address, + portal_contract_address: EthAddress, + function_tree_root: Field, + 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); + } + // 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. From 2bf06d706d72c4dc43c86e965e74b7a34c0e1a35 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 14 Dec 2023 08:55:24 +0000 Subject: [PATCH 03/12] functional contract tree proof --- .../aztec/src/history/contract_inclusion.nr | 19 +++++----------- .../src/e2e_inclusion_proofs_contract.test.ts | 22 +++++++++++++++++++ .../inclusion_proofs_contract/src/main.nr | 7 ++++-- .../pxe/src/simulator_oracle/index.ts | 6 +++-- 4 files changed, 37 insertions(+), 17 deletions(-) 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 230744ce796..a2b26361da7 100644 --- a/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr +++ b/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr @@ -1,21 +1,16 @@ use dep::protocol_types::{ abis::new_contract_data::NewContractData as ContractLeafPreimage, - address::{Address, EthAddress}, - constants::CONTRACT_TREE_HEIGHT, + address::{AztecAddress, EthAddress}, }; use dep::std::merkle::compute_merkle_root; -use crate::abis::new_contract_data::NewContractData as ContractLeafPreimage; use crate::{ context::PrivateContext, - oracle::get_membership_witness::{ - get_membership_witness, - MembershipWitness, - }, + oracle::get_membership_witness::get_contract_membership_witness, }; pub fn prove_contract_inclusion( - contract_address: Address, + contract_address: AztecAddress, portal_contract_address: EthAddress, function_tree_root: Field, block_number: u32, // The block at which we'll prove that the contract exists @@ -25,19 +20,17 @@ pub fn prove_contract_inclusion( let block_header = context.get_block_header(block_number); // 2) Form the contract tree leaf preimage - leaf_preimage = ContractLeafPreimage { + let preimage = ContractLeafPreimage { contract_address, portal_contract_address, function_tree_root, }; // 3) Get the contract tree leaf by hashing the preimage - let leaf_preimage = preimage.hash(); + let contract_leaf = preimage.hash(); // 4) Get the membership witness of the leaf in the contract tree - let contract_tree_id = 2; // TODO(#3443) - let witness: MembershipWitness = - get_membership_witness(block_number, contract_tree_id, contract_leaf); + let witness = get_contract_membership_witness(block_number, contract_leaf); // 5) Prove that the leaf is in the contract tree assert( 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 da4ab9fbb08..728425e548e 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 @@ -1,4 +1,6 @@ import { AccountWallet, AztecAddress, CompleteAddress, Fr, INITIAL_L2_BLOCK_NUM, PXE } from '@aztec/aztec.js'; +import { FunctionSelector, generateFunctionLeaves } from '@aztec/circuits.js'; +import { computeFunctionTreeRoot } from '@aztec/circuits.js/abis'; import { InclusionProofsContract } from '@aztec/noir-contracts/types'; import { jest } from '@jest/globals'; @@ -159,6 +161,26 @@ 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 () => { + // Choose random block number between first block and current block number to test archival node + const blockNumber = await getRandomBlockNumberSinceDeployment(); + + const contractAddress = contract.address; + const portalContractAddress = contract.portalContract; + + const functions = contract.artifact.functions.map(f => ({ + ...f, + selector: FunctionSelector.fromNameAndParameters(f.name, f.parameters), + })); + const functionLeaves = generateFunctionLeaves(functions); + const functionTreeRoot = computeFunctionTreeRoot(functionLeaves); + + await contract.methods + .test_contract_inclusion_proof(contractAddress, portalContractAddress, functionTreeRoot, blockNumber) + .send() + .wait(); + }); + const getRandomBlockNumberSinceDeployment = async () => { const currentBlockNumber = await pxe.getBlockNumber(); return deploymentBlockNumber + Math.floor(Math.random() * (currentBlockNumber - deploymentBlockNumber)); 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 2bfabb6ea65..9fea22adebf 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,6 +1,9 @@ // A demonstration of inclusion and non-inclusion proofs. contract InclusionProofs { - use dep::protocol_types::address::AztecAddress; + use dep::protocol_types::address::{ + AztecAddress, + EthAddress, + }; use dep::aztec::{ state_vars::{ map::Map, @@ -187,7 +190,7 @@ contract InclusionProofs { // of contracts (similarily to what Uniswap Factory does with pool contracts). #[aztec(private)] fn test_contract_inclusion_proof( - contract_address: Address, + contract_address: AztecAddress, portal_contract_address: EthAddress, function_tree_root: Field, block_number: u32, // The block at which we'll prove that the public value exists diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 537d60b654f..7e88e7af79f 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -150,14 +150,16 @@ export class SimulatorOracle implements DBOracle { public async getSiblingPath(blockNumber: number, treeId: MerkleTreeId, leafIndex: bigint): Promise { // @todo Doing a nasty workaround here because of https://github.com/AztecProtocol/aztec-packages/issues/3414 switch (treeId) { + case MerkleTreeId.CONTRACT_TREE: + return (await this.stateInfoProvider.getContractSiblingPath(blockNumber, leafIndex)).toFieldArray(); case MerkleTreeId.NULLIFIER_TREE: return (await this.stateInfoProvider.getNullifierTreeSiblingPath(blockNumber, leafIndex)).toFieldArray(); case MerkleTreeId.NOTE_HASH_TREE: return (await this.stateInfoProvider.getNoteHashSiblingPath(blockNumber, leafIndex)).toFieldArray(); - case MerkleTreeId.ARCHIVE: - return (await this.stateInfoProvider.getArchiveSiblingPath(blockNumber, leafIndex)).toFieldArray(); case MerkleTreeId.PUBLIC_DATA_TREE: return (await this.stateInfoProvider.getPublicDataTreeSiblingPath(blockNumber, leafIndex)).toFieldArray(); + case MerkleTreeId.ARCHIVE: + return (await this.stateInfoProvider.getArchiveSiblingPath(blockNumber, leafIndex)).toFieldArray(); default: throw new Error('Not implemented'); } From 1b88d1d03929b462bba19d753bc4f9db11c34624 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 14 Dec 2023 09:28:55 +0000 Subject: [PATCH 04/12] failure case --- .../aztec/src/history/contract_inclusion.nr | 5 ++ .../src/e2e_inclusion_proofs_contract.test.ts | 49 +++++++++++++++---- .../inclusion_proofs_contract/src/main.nr | 4 +- 3 files changed, 47 insertions(+), 11 deletions(-) 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 a2b26361da7..01e776434ae 100644 --- a/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr +++ b/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr @@ -9,6 +9,11 @@ use crate::{ oracle::get_membership_witness::get_contract_membership_witness, }; +// Proves that the contract address, portal address and function tree root form a valid contract preimage of a leaf +// which exists at block `block_number`. +// 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). pub fn prove_contract_inclusion( contract_address: AztecAddress, portal_contract_address: EthAddress, 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 728425e548e..9f97d9f6278 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 @@ -1,5 +1,13 @@ -import { AccountWallet, AztecAddress, CompleteAddress, Fr, INITIAL_L2_BLOCK_NUM, PXE } from '@aztec/aztec.js'; -import { FunctionSelector, generateFunctionLeaves } from '@aztec/circuits.js'; +import { + AccountWallet, + AztecAddress, + CompleteAddress, + Fr, + FunctionSelector, + INITIAL_L2_BLOCK_NUM, + PXE, +} from '@aztec/aztec.js'; +import { generateFunctionLeaves } from '@aztec/circuits.js'; import { computeFunctionTreeRoot } from '@aztec/circuits.js/abis'; import { InclusionProofsContract } from '@aztec/noir-contracts/types'; @@ -23,6 +31,7 @@ describe('e2e_inclusion_proofs_contract', () => { let contract: InclusionProofsContract; let deploymentBlockNumber: number; const publicValue = 236n; + let contractFunctionTreeRoot: Fr; beforeAll(async () => { ({ pxe, teardown, wallets, accounts } = await setup(1)); @@ -167,13 +176,7 @@ describe('e2e_inclusion_proofs_contract', () => { const contractAddress = contract.address; const portalContractAddress = contract.portalContract; - - const functions = contract.artifact.functions.map(f => ({ - ...f, - selector: FunctionSelector.fromNameAndParameters(f.name, f.parameters), - })); - const functionLeaves = generateFunctionLeaves(functions); - const functionTreeRoot = computeFunctionTreeRoot(functionLeaves); + const functionTreeRoot = getContractFunctionTreeRoot(); await contract.methods .test_contract_inclusion_proof(contractAddress, portalContractAddress, functionTreeRoot, blockNumber) @@ -181,6 +184,22 @@ describe('e2e_inclusion_proofs_contract', () => { .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/); + }); + const getRandomBlockNumberSinceDeployment = async () => { const currentBlockNumber = await pxe.getBlockNumber(); return deploymentBlockNumber + Math.floor(Math.random() * (currentBlockNumber - deploymentBlockNumber)); @@ -190,4 +209,16 @@ 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) { + const functions = contract.artifact.functions.map(f => ({ + ...f, + selector: FunctionSelector.fromNameAndParameters(f.name, f.parameters), + })); + const functionLeaves = generateFunctionLeaves(functions); + contractFunctionTreeRoot = computeFunctionTreeRoot(functionLeaves); + } + 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 9fea22adebf..23189ba8661 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 @@ -186,8 +186,8 @@ contract InclusionProofs { // Proves that the contract address, portal address and function tree root form a valid contract preimage of a leaf // which exists at block `block_number`. // 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 in an internal map - // of contracts (similarily to what Uniswap Factory does with pool contracts). + // 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). #[aztec(private)] fn test_contract_inclusion_proof( contract_address: AztecAddress, From d4da60da28348cdf18d89b9ea6426397e4014564 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 14 Dec 2023 09:44:34 +0000 Subject: [PATCH 05/12] refactor: moved computeContractFunctionTreeRoot function to aztec.js --- yarn-project/aztec.js/src/index.ts | 1 + yarn-project/aztec.js/src/utils/l2_contracts.ts | 17 +++++++++++++++++ .../src/e2e_inclusion_proofs_contract.test.ts | 11 ++--------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index bf7610adc41..4bfe7a298b7 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -42,6 +42,7 @@ 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 67fcdb2851b..6f75e16fea0 100644 --- a/yarn-project/aztec.js/src/utils/l2_contracts.ts +++ b/yarn-project/aztec.js/src/utils/l2_contracts.ts @@ -1,3 +1,6 @@ +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'; @@ -10,3 +13,17 @@ 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/end-to-end/src/e2e_inclusion_proofs_contract.test.ts b/yarn-project/end-to-end/src/e2e_inclusion_proofs_contract.test.ts index 9f97d9f6278..5049223560c 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 @@ -3,12 +3,10 @@ import { AztecAddress, CompleteAddress, Fr, - FunctionSelector, INITIAL_L2_BLOCK_NUM, PXE, + computeContractFunctionTreeRoot, } from '@aztec/aztec.js'; -import { generateFunctionLeaves } from '@aztec/circuits.js'; -import { computeFunctionTreeRoot } from '@aztec/circuits.js/abis'; import { InclusionProofsContract } from '@aztec/noir-contracts/types'; import { jest } from '@jest/globals'; @@ -212,12 +210,7 @@ describe('e2e_inclusion_proofs_contract', () => { const getContractFunctionTreeRoot = () => { if (!contractFunctionTreeRoot) { - const functions = contract.artifact.functions.map(f => ({ - ...f, - selector: FunctionSelector.fromNameAndParameters(f.name, f.parameters), - })); - const functionLeaves = generateFunctionLeaves(functions); - contractFunctionTreeRoot = computeFunctionTreeRoot(functionLeaves); + contractFunctionTreeRoot = computeContractFunctionTreeRoot(contract.artifact); } return contractFunctionTreeRoot; }; From 40ec8a3b9f83b647f9fe684028e416e6b19fcd47 Mon Sep 17 00:00:00 2001 From: benesjan Date: Thu, 14 Dec 2023 09:48:19 +0000 Subject: [PATCH 06/12] naming consistency fix --- yarn-project/aztec-node/src/aztec-node/server.ts | 4 ++-- yarn-project/pxe/src/simulator_oracle/index.ts | 4 ++-- yarn-project/types/src/interfaces/state_info_provider.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 5a1b9884606..ee0e7fb0b79 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -339,7 +339,7 @@ export class AztecNodeService implements AztecNode { * @param leafIndex - The index of the leaf for which the sibling path is required. * @returns The sibling path for the leaf index. */ - public async getNullifierTreeSiblingPath( + public async getNullifierSiblingPath( blockNumber: number | 'latest', leafIndex: bigint, ): Promise> { @@ -408,7 +408,7 @@ export class AztecNodeService implements AztecNode { * @param leafIndex - Index of the leaf in the tree. * @returns The sibling path. */ - public async getPublicDataTreeSiblingPath( + public async getPublicDataSiblingPath( blockNumber: number | 'latest', leafIndex: bigint, ): Promise> { diff --git a/yarn-project/pxe/src/simulator_oracle/index.ts b/yarn-project/pxe/src/simulator_oracle/index.ts index 7e88e7af79f..f228aa8fe8c 100644 --- a/yarn-project/pxe/src/simulator_oracle/index.ts +++ b/yarn-project/pxe/src/simulator_oracle/index.ts @@ -153,11 +153,11 @@ export class SimulatorOracle implements DBOracle { case MerkleTreeId.CONTRACT_TREE: return (await this.stateInfoProvider.getContractSiblingPath(blockNumber, leafIndex)).toFieldArray(); case MerkleTreeId.NULLIFIER_TREE: - return (await this.stateInfoProvider.getNullifierTreeSiblingPath(blockNumber, leafIndex)).toFieldArray(); + return (await this.stateInfoProvider.getNullifierSiblingPath(blockNumber, leafIndex)).toFieldArray(); case MerkleTreeId.NOTE_HASH_TREE: return (await this.stateInfoProvider.getNoteHashSiblingPath(blockNumber, leafIndex)).toFieldArray(); case MerkleTreeId.PUBLIC_DATA_TREE: - return (await this.stateInfoProvider.getPublicDataTreeSiblingPath(blockNumber, leafIndex)).toFieldArray(); + return (await this.stateInfoProvider.getPublicDataSiblingPath(blockNumber, leafIndex)).toFieldArray(); case MerkleTreeId.ARCHIVE: return (await this.stateInfoProvider.getArchiveSiblingPath(blockNumber, leafIndex)).toFieldArray(); default: diff --git a/yarn-project/types/src/interfaces/state_info_provider.ts b/yarn-project/types/src/interfaces/state_info_provider.ts index d643ba10b6a..9384212bda4 100644 --- a/yarn-project/types/src/interfaces/state_info_provider.ts +++ b/yarn-project/types/src/interfaces/state_info_provider.ts @@ -50,7 +50,7 @@ export interface StateInfoProvider { * @returns The sibling path for the leaf index. * TODO: https://github.com/AztecProtocol/aztec-packages/issues/3414 */ - getNullifierTreeSiblingPath( + getNullifierSiblingPath( blockNumber: BlockNumber, leafIndex: bigint, ): Promise>; @@ -103,7 +103,7 @@ export interface StateInfoProvider { * @returns The sibling path. * TODO: https://github.com/AztecProtocol/aztec-packages/issues/3414 */ - getPublicDataTreeSiblingPath( + getPublicDataSiblingPath( blockNumber: BlockNumber, leafIndex: bigint, ): Promise>; From 3485b03217fa9750e17e7c812cb2ddc3d3a1cf95 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 15 Dec 2023 08:49:11 +0000 Subject: [PATCH 07/12] it's working --- .../aztec/src/history/contract_inclusion.nr | 47 +++++++---- .../src/contract_deployer/deploy_method.ts | 4 +- yarn-project/aztec.js/src/index.ts | 1 - .../aztec.js/src/utils/l2_contracts.ts | 17 ---- .../src/contract/contract_deployment_info.ts | 3 +- .../circuits.js/src/types/deployment_info.ts | 4 + .../src/e2e_inclusion_proofs_contract.test.ts | 73 ++++++++++------- .../inclusion_proofs_contract/src/main.nr | 81 +++++++++++++------ .../src/crates/types/src/point.nr | 6 ++ 9 files changed, 146 insertions(+), 90 deletions(-) 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 01e776434ae..7180326edb2 100644 --- a/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr +++ b/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr @@ -1,6 +1,10 @@ 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, }; use dep::std::merkle::compute_merkle_root; @@ -14,34 +18,45 @@ 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, + // 2) Compute the contract address + let contract_address = CompleteAddress::compute( + deployer_public_key, + contract_address_salt, function_tree_root, - }; + constructor_hash + ).address; - // 3) Get the contract tree leaf by hashing the preimage + // 3) Form the contract tree leaf preimage + let preimage = ContractLeafPreimage { contract_address, portal_contract_address, function_tree_root }; + + // 4) Get the contract tree leaf by hashing the preimage let contract_leaf = preimage.hash(); - // 4) Get the membership witness of the leaf in the contract tree + // 5) Get the membership witness of the leaf in the contract tree let witness = get_contract_membership_witness(block_number, contract_leaf); - // 5) Prove that the leaf is in the contract tree + // 6) 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/contract_deployer/deploy_method.ts b/yarn-project/aztec.js/src/contract_deployer/deploy_method.ts index e8a9ad18f28..a6dc144d347 100644 --- a/yarn-project/aztec.js/src/contract_deployer/deploy_method.ts +++ b/yarn-project/aztec.js/src/contract_deployer/deploy_method.ts @@ -69,7 +69,7 @@ export class DeployMethod extends Bas const { chainId, protocolVersion } = await this.pxe.getNodeInfo(); - const { completeAddress, constructorHash, functionTreeRoot } = getContractDeploymentInfo( + const { completeAddress, constructorVkHash, functionTreeRoot } = getContractDeploymentInfo( this.artifact, this.args, contractAddressSalt, @@ -78,7 +78,7 @@ export class DeployMethod extends Bas const contractDeploymentData = new ContractDeploymentData( this.publicKey, - constructorHash, + constructorVkHash, functionTreeRoot, contractAddressSalt, portalContract, diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 4bfe7a298b7..bf7610adc41 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 6f75e16fea0..67fcdb2851b 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/contract/contract_deployment_info.ts b/yarn-project/circuits.js/src/contract/contract_deployment_info.ts index 38a15d1cfcc..39bf1cbb13f 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,8 @@ export function getContractDeploymentInfo( return { completeAddress, - constructorHash: constructorVkHash, + constructorHash, + constructorVkHash, functionTreeRoot, }; } diff --git a/yarn-project/circuits.js/src/types/deployment_info.ts b/yarn-project/circuits.js/src/types/deployment_info.ts index a8abba47126..2cc3b98ddc3 100644 --- a/yarn-project/circuits.js/src/types/deployment_info.ts +++ b/yarn-project/circuits.js/src/types/deployment_info.ts @@ -8,6 +8,10 @@ export type DeploymentInfo = { * The complete address of the deployed contract. */ completeAddress: CompleteAddress; + /** + * The contract's constructor verification key hash. + */ + constructorVkHash: Fr; /** * The contract's constructor hash. */ 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 5049223560c..86216f34b75 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,7 +5,8 @@ import { Fr, INITIAL_L2_BLOCK_NUM, PXE, - computeContractFunctionTreeRoot, + Point, + getContractDeploymentInfo, } from '@aztec/aztec.js'; import { InclusionProofsContract } from '@aztec/noir-contracts/types'; @@ -29,12 +30,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); @@ -172,31 +173,54 @@ describe('e2e_inclusion_proofs_contract', () => { // 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 + + const { constructorHash, functionTreeRoot } = getContractDeploymentInfo( + contractArtifact, + constructorArgs, + contractAddressSalt, + publicKey, + ); + const portalContractAddress = contract.portalContract; - const functionTreeRoot = getContractFunctionTreeRoot(); 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 +231,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 23189ba8661..5b1738d79b5 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 6482295491e..1f6b5da720e 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 From bdbaf9c47beead0fe0642efe26032e69c6cc7855 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 15 Dec 2023 09:19:39 +0000 Subject: [PATCH 08/12] cleanup --- .../src/e2e_inclusion_proofs_contract.test.ts | 345 ++++++++++-------- 1 file changed, 184 insertions(+), 161 deletions(-) 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 86216f34b75..ab434b58947 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 @@ -2,6 +2,7 @@ import { AccountWallet, AztecAddress, CompleteAddress, + EthAddress, Fr, INITIAL_L2_BLOCK_NUM, PXE, @@ -42,185 +43,207 @@ describe('e2e_inclusion_proofs_contract', () => { afterAll(() => teardown()); - it('proves note existence and its nullifier non-existence and nullifier non-existence failure case', async () => { - // Owner of a note - const owner = accounts[0].address; - let noteCreationBlockNumber: number; - { - // Create a note - const value = 100n; - const receipt = await contract.methods.create_note(owner, value).send().wait({ debug: true }); - - noteCreationBlockNumber = receipt.blockNumber!; - const { newCommitments, visibleNotes } = receipt.debugInfo!; - - expect(newCommitments.length).toBe(1); - expect(visibleNotes.length).toBe(1); - const [receivedValue, receivedOwner, _randomness] = visibleNotes[0].note.items; - expect(receivedValue.toBigInt()).toBe(value); - expect(receivedOwner).toEqual(owner.toField()); - } - - { - // Prove note inclusion in a given block. - const ignoredCommitment = 0; // Not ignored only when the note doesn't exist - await contract.methods.test_note_inclusion_proof(owner, noteCreationBlockNumber, ignoredCommitment).send().wait(); - } - - { - // Prove that the note has not been nullified - // TODO(#3535): Prove the nullifier non-inclusion at older block to test archival node. This is currently not - // possible because of issue https://github.com/AztecProtocol/aztec-packages/issues/3535 - const blockNumber = await pxe.getBlockNumber(); - const ignoredNullifier = 0; // Not ignored only when the note doesn't exist - await contract.methods.test_nullifier_non_inclusion_proof(owner, blockNumber, ignoredNullifier).send().wait(); - } - - { - // We test the failure case now --> The proof should fail when the nullifier already exists - const receipt = await contract.methods.nullify_note(owner).send().wait({ debug: true }); - const { newNullifiers } = receipt.debugInfo!; - expect(newNullifiers.length).toBe(2); - - const blockNumber = await pxe.getBlockNumber(); - const nullifier = newNullifiers[1]; - // Note: getLowNullifierMembershipWitness returns the membership witness of the nullifier itself and not - // the low nullifier when the nullifier already exists in the tree and for this reason the execution fails - // on low_nullifier.value < nullifier.value check. + describe('note inclusion and nullifier non-inclusion', () => { + let owner: AztecAddress; + + beforeAll(() => { + owner = accounts[0].address; + }); + + it('proves note existence and its nullifier non-existence and nullifier non-existence failure case', async () => { + // Owner of a note + let noteCreationBlockNumber: number; + { + // Create a note + const value = 100n; + const receipt = await contract.methods.create_note(owner, value).send().wait({ debug: true }); + + noteCreationBlockNumber = receipt.blockNumber!; + const { newCommitments, visibleNotes } = receipt.debugInfo!; + + expect(newCommitments.length).toBe(1); + expect(visibleNotes.length).toBe(1); + const [receivedValue, receivedOwner, _randomness] = visibleNotes[0].note.items; + expect(receivedValue.toBigInt()).toBe(value); + expect(receivedOwner).toEqual(owner.toField()); + } + + { + // Prove note inclusion in a given block. + const ignoredCommitment = 0; // Not ignored only when the note doesn't exist + await contract.methods + .test_note_inclusion_proof(owner, noteCreationBlockNumber, ignoredCommitment) + .send() + .wait(); + } + + { + // Prove that the note has not been nullified + // TODO(#3535): Prove the nullifier non-inclusion at older block to test archival node. This is currently not + // possible because of issue https://github.com/AztecProtocol/aztec-packages/issues/3535 + const blockNumber = await pxe.getBlockNumber(); + const ignoredNullifier = 0; // Not ignored only when the note doesn't exist + await contract.methods.test_nullifier_non_inclusion_proof(owner, blockNumber, ignoredNullifier).send().wait(); + } + + { + // We test the failure case now --> The proof should fail when the nullifier already exists + const receipt = await contract.methods.nullify_note(owner).send().wait({ debug: true }); + const { newNullifiers } = receipt.debugInfo!; + expect(newNullifiers.length).toBe(2); + + const blockNumber = await pxe.getBlockNumber(); + const nullifier = newNullifiers[1]; + // Note: getLowNullifierMembershipWitness returns the membership witness of the nullifier itself and not + // the low nullifier when the nullifier already exists in the tree and for this reason the execution fails + // on low_nullifier.value < nullifier.value check. + await expect( + contract.methods.test_nullifier_non_inclusion_proof(owner, blockNumber, nullifier).send().wait(), + ).rejects.toThrowError( + /Proving nullifier non-inclusion failed: low_nullifier.value < nullifier.value check failed/, + ); + } + }); + + it('proves note validity (note commitment inclusion and nullifier non-inclusion)', async () => { + // Owner of a note + const owner = accounts[0].address; + let noteCreationBlockNumber: number; + { + // Create a note + const value = 100n; + const receipt = await contract.methods.create_note(owner, value).send().wait({ debug: true }); + + noteCreationBlockNumber = receipt.blockNumber!; + const { newCommitments, visibleNotes } = receipt.debugInfo!; + + expect(newCommitments.length).toBe(1); + expect(visibleNotes.length).toBe(1); + const [receivedValue, receivedOwner, _randomness] = visibleNotes[0].note.items; + expect(receivedValue.toBigInt()).toBe(value); + expect(receivedOwner).toEqual(owner.toField()); + } + + { + // Prove note validity + await contract.methods.test_note_validity_proof(owner, noteCreationBlockNumber).send().wait(); + } + }); + + it('note existence failure case', async () => { + // Owner of a note - ignored in the contract since the note won't be found and the spare random note commitment + // will be used instead + const owner = AztecAddress.random(); + + // Choose random block number between deployment and current block number to test archival node + const blockNumber = await getRandomBlockNumberSinceDeployment(); + const randomNoteCommitment = Fr.random(); await expect( - contract.methods.test_nullifier_non_inclusion_proof(owner, blockNumber, nullifier).send().wait(), - ).rejects.toThrowError( - /Proving nullifier non-inclusion failed: low_nullifier.value < nullifier.value check failed/, - ); - } + contract.methods.test_note_inclusion_proof(owner, blockNumber, randomNoteCommitment).send().wait(), + ).rejects.toThrow(/Leaf value: 0x[0-9a-fA-F]+ not found in NOTE_HASH_TREE/); + }); }); - it('proves note validity (note commitment inclusion and nullifier non-inclusion)', async () => { - // Owner of a note - const owner = accounts[0].address; - let noteCreationBlockNumber: number; - { - // Create a note - const value = 100n; - const receipt = await contract.methods.create_note(owner, value).send().wait({ debug: true }); - - noteCreationBlockNumber = receipt.blockNumber!; - const { newCommitments, visibleNotes } = receipt.debugInfo!; - - expect(newCommitments.length).toBe(1); - expect(visibleNotes.length).toBe(1); - const [receivedValue, receivedOwner, _randomness] = visibleNotes[0].note.items; - expect(receivedValue.toBigInt()).toBe(value); - expect(receivedOwner).toEqual(owner.toField()); - } - - { - // Prove note validity - await contract.methods.test_note_validity_proof(owner, noteCreationBlockNumber).send().wait(); - } - }); - - it('note existence failure case', async () => { - // Owner of a note - ignored in the contract since the note won't be found and the spare random note commitment - // will be used instead - const owner = AztecAddress.random(); - - // Choose random block number between deployment and current block number to test archival node - const blockNumber = await getRandomBlockNumberSinceDeployment(); - const randomNoteCommitment = Fr.random(); - await expect( - contract.methods.test_note_inclusion_proof(owner, blockNumber, randomNoteCommitment).send().wait(), - ).rejects.toThrow(/Leaf value: 0x[0-9a-fA-F]+ not found in NOTE_HASH_TREE/); - }); - - it('proves an existence of a public value in private context', async () => { - // Choose random block number between deployment and current block number to test archival node - const blockNumber = await getRandomBlockNumberSinceDeployment(); + describe('public value existence at a slot', () => { + it('proves an existence of a public value in private context', async () => { + // Choose random block number between deployment and current block number to test archival node + const blockNumber = await getRandomBlockNumberSinceDeployment(); - await contract.methods.test_public_value_inclusion_proof(publicValue, blockNumber).send().wait(); - }); + await contract.methods.test_public_value_inclusion_proof(publicValue, blockNumber).send().wait(); + }); - it('public value existence failure case', async () => { - // Choose random block number between first block and current block number to test archival node - const blockNumber = await getRandomBlockNumber(); + it('public value existence failure case', async () => { + // Choose random block number between first block and current block number to test archival node + const blockNumber = await getRandomBlockNumber(); - const randomPublicValue = Fr.random(); - await expect( - contract.methods.test_public_value_inclusion_proof(randomPublicValue, blockNumber).send().wait(), - ).rejects.toThrow(/Public value does not match value in witness/); + const randomPublicValue = Fr.random(); + await expect( + contract.methods.test_public_value_inclusion_proof(randomPublicValue, blockNumber).send().wait(), + ).rejects.toThrow(/Public value does not match value in witness/); + }); }); - it('proves existence of a nullifier in private context', async () => { - // Choose random block number between deployment and current block number to test archival node - const blockNumber = await getRandomBlockNumberSinceDeployment(); - const block = await pxe.getBlock(blockNumber); - const nullifier = block?.newNullifiers[0]; + describe('nullifier inclusion', () => { + it('proves existence of a nullifier in private context', async () => { + // Choose random block number between deployment and current block number to test archival node + const blockNumber = await getRandomBlockNumberSinceDeployment(); + const block = await pxe.getBlock(blockNumber); + const nullifier = block?.newNullifiers[0]; - await contract.methods.test_nullifier_inclusion_proof(nullifier!, blockNumber).send().wait(); - }); + await contract.methods.test_nullifier_inclusion_proof(nullifier!, blockNumber).send().wait(); + }); - it('nullifier existence failure case', async () => { - // Choose random block number between first block and current block number to test archival node - const blockNumber = await getRandomBlockNumber(); - const randomNullifier = Fr.random(); + it('nullifier existence failure case', async () => { + // Choose random block number between first block and current block number to test archival node + const blockNumber = await getRandomBlockNumber(); + const randomNullifier = Fr.random(); - await expect( - contract.methods.test_nullifier_inclusion_proof(randomNullifier, blockNumber).send().wait(), - ).rejects.toThrow(/Low nullifier witness not found for nullifier 0x[0-9a-fA-F]+ at block/); + await expect( + contract.methods.test_nullifier_inclusion_proof(randomNullifier, blockNumber).send().wait(), + ).rejects.toThrow(/Low nullifier witness not found for nullifier 0x[0-9a-fA-F]+ at block/); + }); }); - it('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(); + describe('contract inclusion', () => { + // InclusionProofs contract doesn't have associated public key because it's not an account contract + const publicKey = Point.ZERO; + let functionTreeRoot: Fr; + let constructorHash: Fr; + let portalContractAddress: EthAddress; - const contractArtifact = contract.artifact; + beforeAll(() => { + 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 + const constructorArgs = [publicValue]; - const { constructorHash, functionTreeRoot } = getContractDeploymentInfo( - contractArtifact, - constructorArgs, - contractAddressSalt, - publicKey, - ); - - const portalContractAddress = contract.portalContract; - - await contract.methods - .test_contract_inclusion_proof( - publicKey, + ({ constructorHash, functionTreeRoot } = getContractDeploymentInfo( + contractArtifact, + constructorArgs, contractAddressSalt, - functionTreeRoot, - constructorHash, - portalContractAddress, - blockNumber, - ) - .send() - .wait(); - }); + publicKey, + )); + + portalContractAddress = contract.portalContract; + }); + + it('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(); + + // Note: We pass in preimage of AztecAddress instead of just AztecAddress in order for the contract to be able to + // test that the contract was deployed with correct constructor parameters. + await contract.methods + .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; - // 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/); - // }); + await expect( + contract.methods + .test_contract_inclusion_proof( + publicKey, + contractAddressSalt, + functionTreeRoot, + constructorHash, + portalContractAddress, + 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(); From 9237488c9d440c2ef575a5a715845d049385d355 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 15 Dec 2023 09:27:46 +0000 Subject: [PATCH 09/12] updated docs --- .../aztec-nr/aztec/src/history/contract_inclusion.nr | 8 ++++---- .../src/contracts/inclusion_proofs_contract/src/main.nr | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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 7180326edb2..8e71cc5729f 100644 --- a/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr +++ b/yarn-project/aztec-nr/aztec/src/history/contract_inclusion.nr @@ -13,14 +13,14 @@ use crate::{ oracle::get_membership_witness::get_contract_membership_witness, }; -// Proves that the contract address, portal address and function tree root form a valid contract preimage of a leaf -// which exists at block `block_number`. +// Proves that a contract exists at block `block_number` and returns its address. // 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. +// correct constructor arguments. Typically the factory would store the expected construct hash and assert that +// it is what it expects. The constructor param check is the reason of why we pass in the preimage of contract's +// aztec address instead of just the address. pub fn prove_contract_inclusion( deployer_public_key: Point, contract_address_salt: Field, 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 5b1738d79b5..3eaaaca1d69 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 @@ -195,14 +195,14 @@ contract InclusionProofs { ); } - // Proves that the contract address, portal address and function tree root form a valid contract preimage of a leaf - // which exists at block `block_number`. + // Proves that a contract exists at block `block_number`. // 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. + // correct constructor arguments. Typically the factory would store the expected construct hash and assert + // that it is what it expects. The constructor param check is the reason of why we pass in the preimage of + // contract's aztec address instead of just the address. #[aztec(private)] fn test_contract_inclusion_proof( deployer_public_key: Point, From 1f878be1490e643e1e2ffe097f755268273b73ed Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 15 Dec 2023 10:23:22 +0000 Subject: [PATCH 10/12] adding contract to Nargo.toml --- yarn-project/noir-contracts/Nargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn-project/noir-contracts/Nargo.toml b/yarn-project/noir-contracts/Nargo.toml index bfed9ce4136..609d4c37959 100644 --- a/yarn-project/noir-contracts/Nargo.toml +++ b/yarn-project/noir-contracts/Nargo.toml @@ -10,6 +10,7 @@ members = [ "src/contracts/ecdsa_account_contract", "src/contracts/escrow_contract", "src/contracts/import_test_contract", + "src/contracts/inclusion_proofs_contract", "src/contracts/lending_contract", "src/contracts/parent_contract", "src/contracts/pending_commitments_contract", From 95634100dd8c2ce58611eeb92f480c53139c44dc Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 15 Dec 2023 10:55:25 +0000 Subject: [PATCH 11/12] clarified comment --- .../contracts/inclusion_proofs_contract/src/main.nr | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 3eaaaca1d69..0f42396fe77 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 @@ -97,7 +97,10 @@ contract InclusionProofs { 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 + // Value bellow is only used when the note is not found --> used to test the note inclusion failure case (it + // allows me to pass in random value of note nullifier - I cannot add and fetch a random note from PXE because + // PXE performs note commitment inclusion check when you add a new note). + spare_commitment: Field ) { // 1) Get the note from PXE. let private_values = storage.private_values.at(owner.to_field()); @@ -124,7 +127,10 @@ 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 + // Value bellow is only used when the note is not found --> used to test the nullifier non-inclusion failure + // case (it allows me to pass in random value of note nullifier - I cannot add and fetch a random note from PXE + // because PXE performs note commitment inclusion check when you add a new note). + spare_nullifier: Field ) { // 2) Get the note from PXE let private_values = storage.private_values.at(owner.to_field()); From c6483ae49bfd5c91e1d80514c3438b3010dc45e5 Mon Sep 17 00:00:00 2001 From: benesjan Date: Fri, 15 Dec 2023 11:12:27 +0000 Subject: [PATCH 12/12] exact values in test --- .../src/e2e_inclusion_proofs_contract.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 ab434b58947..9d5f750dc9a 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 @@ -9,6 +9,8 @@ import { Point, getContractDeploymentInfo, } from '@aztec/aztec.js'; +import { NewContractData } from '@aztec/circuits.js'; +import { computeContractLeaf } from '@aztec/circuits.js/abis'; import { InclusionProofsContract } from '@aztec/noir-contracts/types'; import { jest } from '@jest/globals'; @@ -140,7 +142,7 @@ describe('e2e_inclusion_proofs_contract', () => { const randomNoteCommitment = Fr.random(); await expect( contract.methods.test_note_inclusion_proof(owner, blockNumber, randomNoteCommitment).send().wait(), - ).rejects.toThrow(/Leaf value: 0x[0-9a-fA-F]+ not found in NOTE_HASH_TREE/); + ).rejects.toThrow(`Leaf value: ${randomNoteCommitment.toString()} not found in NOTE_HASH_TREE`); }); }); @@ -180,7 +182,7 @@ describe('e2e_inclusion_proofs_contract', () => { await expect( contract.methods.test_nullifier_inclusion_proof(randomNullifier, blockNumber).send().wait(), - ).rejects.toThrow(/Low nullifier witness not found for nullifier 0x[0-9a-fA-F]+ at block/); + ).rejects.toThrow(`Low nullifier witness not found for nullifier ${randomNullifier.toString()} at block`); }); }); @@ -229,6 +231,9 @@ describe('e2e_inclusion_proofs_contract', () => { // This should fail because we choose a block number before the contract was deployed const blockNumber = deploymentBlockNumber - 1; + const contractData = new NewContractData(contract.address, contract.portalContract, functionTreeRoot); + const leaf = computeContractLeaf(contractData); + await expect( contract.methods .test_contract_inclusion_proof( @@ -241,7 +246,7 @@ describe('e2e_inclusion_proofs_contract', () => { ) .send() .wait(), - ).rejects.toThrow(/Leaf value: 0x[0-9a-fA-F]+ not found in CONTRACT_TREE/); + ).rejects.toThrow(`Leaf value: ${leaf.toString()} not found in CONTRACT_TREE`); }); });