diff --git a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr index feee4ecfecc..fb2eaf7c4fd 100644 --- a/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/app_subscription_contract/src/main.nr @@ -1,7 +1,7 @@ mod subscription_note; mod dapp_payload; -contract AppSubscriptionContract { +contract AppSubscription { use dep::std; use dep::std::option::Option; use crate::dapp_payload::DAppPayload; diff --git a/noir-projects/noir-contracts/contracts/fpc_contract/src/main.nr b/noir-projects/noir-contracts/contracts/fpc_contract/src/main.nr index 70333ebcd7e..51b0ba3850a 100644 --- a/noir-projects/noir-contracts/contracts/fpc_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/fpc_contract/src/main.nr @@ -40,9 +40,9 @@ contract FPC { ); let _void = context.call_public_function( - storage.fee_asset.read_private(), - FunctionSelector::from_signature("pay_fee(Field)"), - [amount.to_field()] + context.this_address(), + FunctionSelector::from_signature("pay_fee((Field),Field,(Field))"), + [context.msg_sender().to_field(), amount, asset.to_field()] ); } @@ -67,12 +67,14 @@ contract FPC { } #[aztec(public)] - internal fn pay_fee(from: AztecAddress, amount: Field, asset: AztecAddress) { - let _void = context.call_public_function( + internal fn pay_fee(refund_address: AztecAddress, amount: Field, asset: AztecAddress) { + let refund = context.call_public_function( storage.fee_asset.read_public(), FunctionSelector::from_signature("pay_fee(Field)"), - [amount.to_field()] - ); - // TODO handle refunds + [amount] + )[0]; + + // Just do public refunds for the present + Token::at(asset).transfer_public(context, context.this_address(), refund_address, refund, 0) } } diff --git a/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr index 736e5777b12..83fd519e47b 100644 --- a/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/gas_token_contract/src/main.nr @@ -24,6 +24,17 @@ contract GasToken { storage.balances.at(to).write(new_balance); } + // TODO(@just-mitch): remove this function before mainnet deployment + // convenience function for testing + // the true canonical gas token contract will not have this function + #[aztec(public)] + fn mint_public(to: AztecAddress, amount: Field) { + let amount = U128::from_integer(amount); + let new_balance = storage.balances.at(to).read().add(amount); + + storage.balances.at(to).write(new_balance); + } + #[aztec(public)] fn check_balance(fee_limit: Field) { let fee_limit = U128::from_integer(fee_limit); diff --git a/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_teardown.nr b/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_teardown.nr index 637f29c4771..7139187956c 100644 --- a/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_teardown.nr +++ b/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_teardown.nr @@ -49,7 +49,13 @@ impl PublicKernelTeardownCircuitPrivateInputs { &mut public_inputs ); - public_inputs.to_inner() + let mut output = public_inputs.to_inner(); + + // If we enqueued multiple public functions as part of executing the teardown circuit, + // continue to treat them as part of teardown. + output.needs_setup = false; + + output } } diff --git a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/base_rollup_inputs.nr b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/base_rollup_inputs.nr index d7e520eb3c0..fd70d6535b4 100644 --- a/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/base_rollup_inputs.nr +++ b/noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/base_rollup_inputs.nr @@ -13,7 +13,7 @@ use dep::types::{ PublicDataMembershipWitness }, nullifier_leaf_preimage::NullifierLeafPreimage, public_data_update_request::PublicDataUpdateRequest, - public_data_read::PublicDataRead, kernel_data::PublicKernelData, + public_data_read::PublicDataRead, kernel_data::RollupKernelData, side_effect::{SideEffect, SideEffectLinkedToNoteHash}, accumulated_data::CombinedAccumulatedData }, constants::{ @@ -38,7 +38,7 @@ use dep::types::{ }; struct BaseRollupInputs { - kernel_data: PublicKernelData, + kernel_data: RollupKernelData, start: PartialStateReference, state_diff_hints: StateDiffHints, @@ -72,18 +72,12 @@ impl BaseRollupInputs { == self.constants.global_variables.version, "kernel version does not match the rollup version" ); - // recombine the accumulated data - let combined = CombinedAccumulatedData::recombine( - self.kernel_data.public_inputs.end_non_revertible, - self.kernel_data.public_inputs.end - ); - // First we compute the contract tree leaves - let contract_leaves = self.calculate_contract_leaves(combined); + let contract_leaves = self.calculate_contract_leaves(); let contracts_tree_subroot = self.calculate_contract_subtree_root(contract_leaves); - let commitments_tree_subroot = self.calculate_commitments_subtree(combined); + let commitments_tree_subroot = self.calculate_commitments_subtree(); let empty_commitments_subtree_root = calculate_empty_tree_root(NOTE_HASH_SUBTREE_HEIGHT); @@ -106,13 +100,13 @@ impl BaseRollupInputs { ); // Insert nullifiers: - let end_nullifier_tree_snapshot = self.check_nullifier_tree_non_membership_and_insert_to_tree(combined); + let end_nullifier_tree_snapshot = self.check_nullifier_tree_non_membership_and_insert_to_tree(); // Validate public public data reads and public data update requests, and update public data tree - let end_public_data_tree_snapshot = self.validate_and_process_public_state(combined); + let end_public_data_tree_snapshot = self.validate_and_process_public_state(); // Calculate the overall calldata hash - let calldata_hash = BaseRollupInputs::components_compute_kernel_calldata_hash(combined); + let calldata_hash = BaseRollupInputs::components_compute_kernel_calldata_hash(self.kernel_data.public_inputs.end); // Perform membership checks that the notes provided exist within the historical trees data self.perform_archive_membership_checks(); @@ -135,9 +129,9 @@ impl BaseRollupInputs { } } - fn calculate_contract_leaves(self, combined: CombinedAccumulatedData) -> [Field; MAX_NEW_CONTRACTS_PER_TX] { + fn calculate_contract_leaves(self) -> [Field; MAX_NEW_CONTRACTS_PER_TX] { let mut contract_leaves = [0; MAX_NEW_CONTRACTS_PER_TX]; - let new_contracts = combined.new_contracts; + let new_contracts = self.kernel_data.public_inputs.end.new_contracts; // loop over the new contracts for i in 0..new_contracts.len() { @@ -164,14 +158,14 @@ impl BaseRollupInputs { // TODO(Kev): This should say calculate_commitments_subtree_root // Cpp code says calculate_commitments_subtree, so I'm leaving it as is for now - fn calculate_commitments_subtree(self, combined: CombinedAccumulatedData) -> Field { - calculate_subtree(combined.new_note_hashes.map(|c: SideEffect| c.value)) + fn calculate_commitments_subtree(self) -> Field { + calculate_subtree(self.kernel_data.public_inputs.end.new_note_hashes.map(|c: SideEffect| c.value)) } - fn check_nullifier_tree_non_membership_and_insert_to_tree(self, combined: CombinedAccumulatedData) -> AppendOnlyTreeSnapshot { + fn check_nullifier_tree_non_membership_and_insert_to_tree(self) -> AppendOnlyTreeSnapshot { indexed_tree::batch_insert( self.start.nullifier_tree, - combined.new_nullifiers.map(|nullifier: SideEffectLinkedToNoteHash| nullifier.value), + self.kernel_data.public_inputs.end.new_nullifiers.map(|nullifier: SideEffectLinkedToNoteHash| nullifier.value), self.state_diff_hints.sorted_nullifiers, self.state_diff_hints.sorted_nullifier_indexes, self.state_diff_hints.nullifier_subtree_sibling_path, @@ -219,7 +213,7 @@ impl BaseRollupInputs { calculate_subtree(leaves.map(|leaf:NullifierLeafPreimage| leaf.hash())) } - fn validate_and_process_public_state(self, combined: CombinedAccumulatedData) -> AppendOnlyTreeSnapshot { + fn validate_and_process_public_state(self) -> AppendOnlyTreeSnapshot { // TODO(#2521) - data read validation should happen against the current state of the tx and not the start state. // Blocks all interesting usecases that read and write to the same public state in the same tx. // https://aztecprotocol.slack.com/archives/C02M7VC7TN0/p1695809629015719?thread_ts=1695653252.007339&cid=C02M7VC7TN0 @@ -233,7 +227,7 @@ impl BaseRollupInputs { let end_public_data_tree_snapshot = insert_public_data_update_requests( self.start.public_data_tree, - combined.public_data_update_requests.map( + self.kernel_data.public_inputs.end.public_data_update_requests.map( |request: PublicDataUpdateRequest| { PublicDataTreeLeaf { slot: request.leaf_slot, @@ -566,7 +560,7 @@ mod tests { membership_witness::{ArchiveRootMembershipWitness, NullifierMembershipWitness, PublicDataMembershipWitness}, new_contract_data::NewContractData, nullifier_leaf_preimage::NullifierLeafPreimage, public_data_read::PublicDataRead, public_data_update_request::PublicDataUpdateRequest, - kernel_data::PublicKernelData, side_effect::SideEffect, + kernel_data::{PublicKernelData, RollupKernelData}, side_effect::SideEffect, accumulated_data::CombinedAccumulatedData }, address::{AztecAddress, EthAddress}, @@ -620,7 +614,7 @@ mod tests { fn update_public_data_tree( public_data_tree: &mut NonEmptyMerkleTree, - kernel_data: &mut PublicKernelData, + kernel_data: &mut RollupKernelData, snapshot: AppendOnlyTreeSnapshot, public_data_writes: BoundedVec<(u64, PublicDataTreeLeaf), 2>, mut pre_existing_public_data: [PublicDataTreeLeafPreimage; EXISTING_LEAVES] @@ -744,7 +738,7 @@ mod tests { fn update_nullifier_tree_with_new_leaves( mut self, nullifier_tree: &mut NonEmptyMerkleTree, - kernel_data: &mut PublicKernelData, + kernel_data: &mut RollupKernelData, start_nullifier_tree_snapshot: AppendOnlyTreeSnapshot ) -> ([NullifierLeafPreimage; MAX_NEW_NULLIFIERS_PER_TX], [NullifierMembershipWitness; MAX_NEW_NULLIFIERS_PER_TX], [Field; MAX_NEW_NULLIFIERS_PER_TX], [u64; MAX_NEW_NULLIFIERS_PER_TX]) { let mut nullifier_predecessor_preimages: [NullifierLeafPreimage; MAX_NEW_NULLIFIERS_PER_TX] = dep::std::unsafe::zeroed(); @@ -801,12 +795,7 @@ mod tests { } fn build_inputs(mut self) -> BaseRollupInputs { - let mut kernel_data = self.kernel_data.to_public_kernel_data(); - - let combined = CombinedAccumulatedData::recombine( - kernel_data.public_inputs.end_non_revertible, - kernel_data.public_inputs.end - ); + let mut kernel_data = self.kernel_data.to_rollup_kernel_data(); let start_note_hash_tree = NonEmptyMerkleTree::new( self.pre_existing_notes, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/kernel_data.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/kernel_data.nr index aa562c45ec9..b1c0539b813 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/kernel_data.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/kernel_data.nr @@ -43,3 +43,10 @@ struct PublicKernelData { vk_path : [Field; VK_TREE_HEIGHT], } +struct RollupKernelData { + public_inputs : RollupKernelCircuitPublicInputs, + proof : Proof, + vk : VerificationKey, + vk_index : u32, + vk_path : [Field; VK_TREE_HEIGHT], +} diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/tests/kernel_data_builder.nr b/noir-projects/noir-protocol-circuits/crates/types/src/tests/kernel_data_builder.nr index 8645bd06fb8..5f370ce3b57 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/tests/kernel_data_builder.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/tests/kernel_data_builder.nr @@ -7,7 +7,7 @@ use crate::{ PrivateKernelInnerCircuitPublicInputs, PrivateKernelTailCircuitPublicInputs, PublicKernelCircuitPublicInputs, RollupKernelCircuitPublicInputs }, - kernel_data::{PrivateKernelInnerData, PrivateKernelTailData, PublicKernelData}, + kernel_data::{PrivateKernelInnerData, PrivateKernelTailData, PublicKernelData, RollupKernelData}, public_data_read::PublicDataRead, public_data_update_request::PublicDataUpdateRequest, read_request::ReadRequestContext, side_effect::{SideEffect, SideEffectLinkedToNoteHash} }, @@ -292,4 +292,14 @@ impl PreviousKernelDataBuilder { PublicKernelData { public_inputs, proof: self.proof, vk: self.vk, vk_index: self.vk_index, vk_path: self.vk_path } } + + pub fn to_rollup_kernel_data(self) -> RollupKernelData { + let public_inputs = RollupKernelCircuitPublicInputs { + aggregation_object: AggregationObject {}, + end: self.end.finish(), + constants: CombinedConstantData { historical_header: self.historical_header, tx_context: self.tx_context } + }; + + RollupKernelData { public_inputs, proof: self.proof, vk: self.vk, vk_index: self.vk_index, vk_path: self.vk_path } + } } diff --git a/yarn-project/circuits.js/jest.config.ts b/yarn-project/circuits.js/jest.config.ts new file mode 100644 index 00000000000..83d85d85f9b --- /dev/null +++ b/yarn-project/circuits.js/jest.config.ts @@ -0,0 +1,12 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest/presets/default-esm', + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.[cm]?js$': '$1', + }, + testRegex: './src/.*\\.test\\.(js|mjs|ts)$', + rootDir: './src', +}; + +export default config; diff --git a/yarn-project/circuits.js/package.json b/yarn-project/circuits.js/package.json index 344a8f220f1..1384c153fc4 100644 --- a/yarn-project/circuits.js/package.json +++ b/yarn-project/circuits.js/package.json @@ -29,17 +29,6 @@ "remake-constants": "node --loader ts-node/esm src/scripts/constants.in.ts && prettier -w src/constants.gen.ts && cd ../../l1-contracts && ./.foundry/bin/forge fmt", "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --passWithNoTests" }, - "inherits": [ - "../package.common.json" - ], - "jest": { - "preset": "ts-jest/presets/default-esm", - "moduleNameMapper": { - "^(\\.{1,2}/.*)\\.[cm]?js$": "$1" - }, - "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$", - "rootDir": "./src" - }, "dependencies": { "@aztec/bb.js": "portal:../../barretenberg/ts", "@aztec/foundation": "workspace:^", diff --git a/yarn-project/circuits.js/src/structs/index.ts b/yarn-project/circuits.js/src/structs/index.ts index 408eb21bcf7..eec426b81a2 100644 --- a/yarn-project/circuits.js/src/structs/index.ts +++ b/yarn-project/circuits.js/src/structs/index.ts @@ -3,13 +3,13 @@ export * from './aggregation_object.js'; export * from './call_context.js'; export * from './call_request.js'; export * from './complete_address.js'; +export * from './content_commitment.js'; export * from './contract_deployment_data.js'; export * from './contract_storage_read.js'; export * from './contract_storage_update_request.js'; export * from './function_data.js'; export * from './function_leaf_preimage.js'; export * from './global_variables.js'; -export * from './content_commitment.js'; export * from './header.js'; export * from './kernel/combined_accumulated_data.js'; export * from './kernel/combined_constant_data.js'; @@ -26,6 +26,8 @@ export * from './kernel/public_call_data.js'; export * from './kernel/public_kernel_circuit_private_inputs.js'; export * from './kernel/public_kernel_circuit_public_inputs.js'; export * from './kernel/public_kernel_data.js'; +export * from './kernel/rollup_kernel_circuit_public_inputs.js'; +export * from './kernel/rollup_kernel_data.js'; export * from './l2_to_l1_message.js'; export * from './membership_witness.js'; export * from './nullifier_key_validation_request.js'; diff --git a/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.test.ts b/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.test.ts index a91075fa051..53725a9e0cc 100644 --- a/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.test.ts +++ b/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.test.ts @@ -1,9 +1,9 @@ -import { makeAccumulatedData, makeFinalAccumulatedData } from '../../tests/factories.js'; +import { makeCombinedAccumulatedData, makeFinalAccumulatedData } from '../../tests/factories.js'; import { CombinedAccumulatedData, PrivateAccumulatedRevertibleData } from './combined_accumulated_data.js'; describe('CombinedAccumulatedData', () => { it('Data after serialization and deserialization is equal to the original', () => { - const original = makeAccumulatedData(); + const original = makeCombinedAccumulatedData(); const afterSerialization = CombinedAccumulatedData.fromBuffer(original.toBuffer()); expect(original).toEqual(afterSerialization); }); diff --git a/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.ts b/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.ts index a62942064ae..f15f9c5afff 100644 --- a/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.ts +++ b/yarn-project/circuits.js/src/structs/kernel/combined_accumulated_data.ts @@ -86,6 +86,10 @@ export class PublicDataRead { toFriendlyJSON() { return `Leaf=${this.leafSlot.toFriendlyJSON()}: ${this.value.toFriendlyJSON()}`; } + + equals(other: PublicDataRead) { + return this.leafSlot.equals(other.leafSlot) && this.value.equals(other.value); + } } /** @@ -128,6 +132,10 @@ export class PublicDataUpdateRequest { return this.leafSlot.isZero() && this.newValue.isZero(); } + static isEmpty(x: PublicDataUpdateRequest) { + return x.isEmpty(); + } + equals(other: PublicDataUpdateRequest) { return this.leafSlot.equals(other.leafSlot) && this.newValue.equals(other.newValue); } @@ -325,8 +333,22 @@ export class CombinedAccumulatedData { MAX_PUBLIC_CALL_STACK_LENGTH_PER_TX, ); + const nonSquashedWrites = [ + ...revertible.publicDataUpdateRequests, + ...nonRevertible.publicDataUpdateRequests, + ].filter(x => !x.isEmpty()); + + const squashedWrites = Array.from( + nonSquashedWrites + .reduce>((acc, curr) => { + acc.set(curr.leafSlot.toString(), curr); + return acc; + }, new Map()) + .values(), + ); + const publicDataUpdateRequests = padArrayEnd( - [...nonRevertible.publicDataUpdateRequests, ...revertible.publicDataUpdateRequests].filter(x => !x.isEmpty()), + squashedWrites, PublicDataUpdateRequest.empty(), MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, ); diff --git a/yarn-project/circuits.js/src/structs/kernel/rollup_kernel_circuit_public_inputs.ts b/yarn-project/circuits.js/src/structs/kernel/rollup_kernel_circuit_public_inputs.ts new file mode 100644 index 00000000000..9d97ce38ed5 --- /dev/null +++ b/yarn-project/circuits.js/src/structs/kernel/rollup_kernel_circuit_public_inputs.ts @@ -0,0 +1,52 @@ +import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; + +import { AggregationObject } from '../aggregation_object.js'; +import { CombinedAccumulatedData } from './combined_accumulated_data.js'; +import { CombinedConstantData } from './combined_constant_data.js'; + +/** + * Outputs from the public kernel circuits. + * All Public kernels use this shape for outputs. + */ +export class RollupKernelCircuitPublicInputs { + constructor( + /** + * Aggregated proof of all the previous kernel iterations. + */ + public aggregationObject: AggregationObject, // Contains the aggregated proof of all previous kernel iterations + /** + * Data accumulated from both public and private circuits. + */ + public end: CombinedAccumulatedData, + /** + * Data which is not modified by the circuits. + */ + public constants: CombinedConstantData, + ) {} + + toBuffer() { + return serializeToBuffer(this.aggregationObject, this.end, this.constants); + } + + /** + * Deserializes from a buffer or reader, corresponding to a write in cpp. + * @param buffer - Buffer or reader to read from. + * @returns A new instance of RollupKernelCircuitPublicInputs. + */ + static fromBuffer(buffer: Buffer | BufferReader): RollupKernelCircuitPublicInputs { + const reader = BufferReader.asReader(buffer); + return new RollupKernelCircuitPublicInputs( + reader.readObject(AggregationObject), + reader.readObject(CombinedAccumulatedData), + reader.readObject(CombinedConstantData), + ); + } + + static empty() { + return new RollupKernelCircuitPublicInputs( + AggregationObject.makeFake(), + CombinedAccumulatedData.empty(), + CombinedConstantData.empty(), + ); + } +} diff --git a/yarn-project/circuits.js/src/structs/kernel/rollup_kernel_data.ts b/yarn-project/circuits.js/src/structs/kernel/rollup_kernel_data.ts new file mode 100644 index 00000000000..9945403bdcc --- /dev/null +++ b/yarn-project/circuits.js/src/structs/kernel/rollup_kernel_data.ts @@ -0,0 +1,66 @@ +import { makeTuple } from '@aztec/foundation/array'; +import { Fr } from '@aztec/foundation/fields'; +import { BufferReader, Tuple, serializeToBuffer } from '@aztec/foundation/serialize'; + +import { VK_TREE_HEIGHT } from '../../constants.gen.js'; +import { Proof, makeEmptyProof } from '../proof.js'; +import { UInt32 } from '../shared.js'; +import { VerificationKey } from '../verification_key.js'; +import { RollupKernelCircuitPublicInputs } from './rollup_kernel_circuit_public_inputs.js'; + +/** + * Data of the previous public kernel iteration in the chain of kernels. + */ +export class RollupKernelData { + constructor( + /** + * Public inputs of the previous kernel. + */ + public publicInputs: RollupKernelCircuitPublicInputs, + /** + * Proof of the previous kernel. + */ + public proof: Proof, + /** + * Verification key of the previous kernel. + */ + public vk: VerificationKey, + /** + * Index of the previous kernel's vk in a tree of vks. + */ + public vkIndex: UInt32, + /** + * Sibling path of the previous kernel's vk in a tree of vks. + */ + public vkPath: Tuple, + ) {} + + static fromBuffer(buffer: Buffer | BufferReader): RollupKernelData { + const reader = BufferReader.asReader(buffer); + return new this( + reader.readObject(RollupKernelCircuitPublicInputs), + reader.readObject(Proof), + reader.readObject(VerificationKey), + reader.readNumber(), + reader.readArray(VK_TREE_HEIGHT, Fr), + ); + } + + static empty(): RollupKernelData { + return new this( + RollupKernelCircuitPublicInputs.empty(), + makeEmptyProof(), + VerificationKey.makeFake(), + 0, + makeTuple(VK_TREE_HEIGHT, Fr.zero), + ); + } + + /** + * Serialize this as a buffer. + * @returns The buffer. + */ + toBuffer() { + return serializeToBuffer(this.publicInputs, this.proof, this.vk, this.vkIndex, this.vkPath); + } +} diff --git a/yarn-project/circuits.js/src/structs/rollup/base_rollup.ts b/yarn-project/circuits.js/src/structs/rollup/base_rollup.ts index a8b03f70527..cbbfaf491ac 100644 --- a/yarn-project/circuits.js/src/structs/rollup/base_rollup.ts +++ b/yarn-project/circuits.js/src/structs/rollup/base_rollup.ts @@ -9,7 +9,7 @@ import { PUBLIC_DATA_TREE_HEIGHT, } from '../../constants.gen.js'; import { GlobalVariables } from '../global_variables.js'; -import { PublicKernelData } from '../kernel/public_kernel_data.js'; +import { RollupKernelData } from '../kernel/rollup_kernel_data.js'; import { MembershipWitness } from '../membership_witness.js'; import { PartialStateReference } from '../partial_state_reference.js'; import { UInt32 } from '../shared.js'; @@ -88,7 +88,7 @@ export class ConstantRollupData { export class BaseRollupInputs { constructor( /** Data of the 2 kernels that preceded this base rollup circuit. */ - public kernelData: PublicKernelData, + public kernelData: RollupKernelData, /** Partial state reference at the start of the rollup. */ public start: PartialStateReference, /** Hints used while proving state diff validity. */ diff --git a/yarn-project/circuits.js/src/structs/rollup/public_data_leaf/index.ts b/yarn-project/circuits.js/src/structs/rollup/public_data_leaf/index.ts index 87743ba59a0..5724f220e02 100644 --- a/yarn-project/circuits.js/src/structs/rollup/public_data_leaf/index.ts +++ b/yarn-project/circuits.js/src/structs/rollup/public_data_leaf/index.ts @@ -110,6 +110,14 @@ export class PublicDataTreeLeaf implements IndexedTreeLeaf { return new PublicDataTreeLeaf(Fr.fromBuffer(reader), Fr.fromBuffer(reader)); } + equals(another: PublicDataTreeLeaf): boolean { + return this.slot.equals(another.slot) && this.value.equals(another.value); + } + + toString(): string { + return `PublicDataTreeLeaf(${this.slot.toString()}, ${this.value.toString()})`; + } + isEmpty(): boolean { return this.slot.isZero() && this.value.isZero(); } diff --git a/yarn-project/circuits.js/src/tests/factories.ts b/yarn-project/circuits.js/src/tests/factories.ts index fddeacf2aaf..b29457e88ce 100644 --- a/yarn-project/circuits.js/src/tests/factories.ts +++ b/yarn-project/circuits.js/src/tests/factories.ts @@ -129,6 +129,8 @@ import { GlobalVariables } from '../structs/global_variables.js'; import { Header } from '../structs/header.js'; import { PrivateKernelInitCircuitPrivateInputs } from '../structs/kernel/private_kernel_init_circuit_private_inputs.js'; import { PrivateKernelInnerCircuitPrivateInputs } from '../structs/kernel/private_kernel_inner_circuit_private_inputs.js'; +import { RollupKernelCircuitPublicInputs } from '../structs/kernel/rollup_kernel_circuit_public_inputs.js'; +import { RollupKernelData } from '../structs/kernel/rollup_kernel_data.js'; /** * Creates an arbitrary side effect object with the given seed. @@ -256,7 +258,7 @@ export function makeContractStorageRead(seed = 1): ContractStorageRead { * @param seed - The seed to use for generating the accumulated data. * @returns An accumulated data. */ -export function makeAccumulatedData(seed = 1, full = false): CombinedAccumulatedData { +export function makeCombinedAccumulatedData(seed = 1, full = false): CombinedAccumulatedData { const tupleGenerator = full ? makeTuple : makeHalfFullTuple; return new CombinedAccumulatedData( @@ -453,6 +455,22 @@ export function makePublicKernelCircuitPublicInputs( true, ); } + +/** + * Creates arbitrary public kernel circuit public inputs. + * @param seed - The seed to use for generating the kernel circuit public inputs. + * @returns Public kernel circuit public inputs. + */ +export function makeRollupKernelCircuitPublicInputs( + seed = 1, + fullAccumulatedData = true, +): RollupKernelCircuitPublicInputs { + return new RollupKernelCircuitPublicInputs( + makeAggregationObject(seed), + makeCombinedAccumulatedData(seed, fullAccumulatedData), + makeConstantData(seed + 0x100), + ); +} /** * Creates arbitrary private kernel inner circuit public inputs. * @param seed - The seed to use for generating the kernel circuit public inputs. @@ -465,7 +483,7 @@ export function makePrivateKernelInnerCircuitPublicInputs( return new PrivateKernelInnerCircuitPublicInputs( makeAggregationObject(seed), fr(seed + 0x100), - makeAccumulatedData(seed, full), + makeCombinedAccumulatedData(seed, full), makeConstantData(seed + 0x100), true, ); @@ -608,6 +626,22 @@ export function makePublicKernelData(seed = 1, kernelPublicInputs?: PublicKernel ); } +/** + * Makes arbitrary public kernel data. + * @param seed - The seed to use for generating the previous kernel data. + * @param kernelPublicInputs - The public kernel public inputs to use for generating the public kernel data. + * @returns A previous kernel data. + */ +export function makeRollupKernelData(seed = 1, kernelPublicInputs?: RollupKernelCircuitPublicInputs): RollupKernelData { + return new RollupKernelData( + kernelPublicInputs ?? makeRollupKernelCircuitPublicInputs(seed, true), + new Proof(Buffer.alloc(16, seed + 0x80)), + makeVerificationKey(), + 0x42, + makeTuple(VK_TREE_HEIGHT, fr, 0x1000), + ); +} + /** * Makes arbitrary previous kernel data. * @param seed - The seed to use for generating the previous kernel data. @@ -1167,7 +1201,7 @@ export function makeStateDiffHints(seed = 1): StateDiffHints { * @returns A base rollup inputs. */ export function makeBaseRollupInputs(seed = 0): BaseRollupInputs { - const kernelData = makePublicKernelData(seed); + const kernelData = makeRollupKernelData(seed); const start = makePartialStateReference(seed + 0x100); 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 2e1a031da41..4eabfd385f4 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 @@ -96,7 +96,7 @@ Rollup Address: 0x0dcd1bf9a1b36ce34237eeafef220932846bcd82 const docs = ` // docs:start:example-contracts % aztec-cli example-contracts -AppSubscriptionContractContractArtifact +AppSubscriptionContractArtifact BenchmarkingContractArtifact CardGameContractArtifact ChildContractArtifact diff --git a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts index 20109713d32..0f3af47c88f 100644 --- a/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts +++ b/yarn-project/end-to-end/src/e2e_dapp_subscription.test.ts @@ -10,7 +10,7 @@ import { } from '@aztec/aztec.js'; import { DefaultDappEntrypoint } from '@aztec/entrypoints/dapp'; import { - AppSubscriptionContractContract, + AppSubscriptionContract, TokenContract as BananaCoin, CounterContract, FPCContract, @@ -19,22 +19,16 @@ import { import { jest } from '@jest/globals'; -import { - EndToEndContext, - PublicBalancesFn, - assertPublicBalances, - getPublicBalancesFn, - setup, -} from './fixtures/utils.js'; -import { GasBridgingTestHarness } from './shared/gas_portal_test_harness.js'; +import { BalancesFn, EndToEndContext, expectMapping, getBalancesFn, setup } from './fixtures/utils.js'; +import { GasPortalTestingHarnessFactory, IGasBridgingTestHarness } from './shared/gas_portal_test_harness.js'; jest.setTimeout(1_000_000); const TOKEN_NAME = 'BananaCoin'; -const TOKEN_SYMBOL = 'BAC'; +const TOKEN_SYMBOL = 'BC'; const TOKEN_DECIMALS = 18n; -describe('e2e_fees', () => { +describe('e2e_dapp_subscription', () => { let aliceWallet: AccountWalletWithPrivateKey; let bobWallet: AccountWalletWithPrivateKey; let aliceAddress: AztecAddress; // Dapp subscriber. @@ -43,17 +37,23 @@ describe('e2e_fees', () => { // let gasTokenContract: GasTokenContract; let bananaCoin: BananaCoin; let counterContract: CounterContract; - let subscriptionContract: AppSubscriptionContractContract; + let subscriptionContract: AppSubscriptionContract; let gasTokenContract: GasTokenContract; let bananaFPC: FPCContract; let e2eContext: EndToEndContext; - let gasBridgeTestHarness: GasBridgingTestHarness; - let gasBalances: PublicBalancesFn; + let gasBridgeTestHarness: IGasBridgingTestHarness; + let gasBalances: BalancesFn; + let bananasPublicBalances: BalancesFn; + let bananasPrivateBalances: BalancesFn; - const FEE_AMOUNT = 1n; const SUBSCRIPTION_AMOUNT = 100n; const BRIDGED_GAS_BALANCE = 1000n; - const MINTED_BANANAS = 1000n; + const PUBLICLY_MINTED_BANANAS = 500n; + const PRIVATELY_MINTED_BANANAS = 600n; + + const FEE_AMOUNT = 1n; + const REFUND = 2n; // intentionally overpay the gas fee. This is the expected refund. + const MAX_FEE = FEE_AMOUNT + REFUND; beforeAll(async () => { process.env.PXE_URL = ''; @@ -65,13 +65,14 @@ describe('e2e_fees', () => { bobAddress = accounts.at(1)!.address; sequencerAddress = accounts.at(2)!.address; - gasBridgeTestHarness = await GasBridgingTestHarness.new( - pxe, - deployL1ContractsValues.publicClient, - deployL1ContractsValues.walletClient, - wallets[0], + gasBridgeTestHarness = await GasPortalTestingHarnessFactory.create({ + pxeService: pxe, + publicClient: deployL1ContractsValues.publicClient, + walletClient: deployL1ContractsValues.walletClient, + wallet: wallets[0], logger, - ); + mockL1: true, + }); gasTokenContract = gasBridgeTestHarness.l2Token; @@ -88,7 +89,7 @@ describe('e2e_fees', () => { counterContract = await CounterContract.deploy(bobWallet, 0, bobAddress).send().deployed(); - subscriptionContract = await AppSubscriptionContractContract.deploy( + subscriptionContract = await AppSubscriptionContract.deploy( bobWallet, counterContract.address, bobAddress, @@ -103,52 +104,92 @@ describe('e2e_fees', () => { // mint some test tokens for Alice // she'll pay for the subscription with these - await bananaCoin.methods.privately_mint_private_note(MINTED_BANANAS).send().wait(); - await bananaCoin.methods.mint_public(aliceAddress, MINTED_BANANAS).send().wait(); + await bananaCoin.methods.privately_mint_private_note(PRIVATELY_MINTED_BANANAS).send().wait(); + await bananaCoin.methods.mint_public(aliceAddress, PUBLICLY_MINTED_BANANAS).send().wait(); await gasBridgeTestHarness.bridgeFromL1ToL2(BRIDGED_GAS_BALANCE, BRIDGED_GAS_BALANCE, subscriptionContract.address); await gasBridgeTestHarness.bridgeFromL1ToL2(BRIDGED_GAS_BALANCE, BRIDGED_GAS_BALANCE, bananaFPC.address); - gasBalances = getPublicBalancesFn('⛽', gasTokenContract, e2eContext.logger); + gasBalances = getBalancesFn('⛽', gasTokenContract.methods.balance_of_public, e2eContext.logger); + bananasPublicBalances = getBalancesFn('Public 🍌', bananaCoin.methods.balance_of_public, e2eContext.logger); + bananasPrivateBalances = getBalancesFn('Private 🍌', bananaCoin.methods.balance_of_private, e2eContext.logger); - await assertPublicBalances( + await expectMapping( gasBalances, - [sequencerAddress, subscriptionContract.address, bananaFPC.address], - [0n, BRIDGED_GAS_BALANCE, BRIDGED_GAS_BALANCE], + [aliceAddress, sequencerAddress, subscriptionContract.address, bananaFPC.address], + [0n, 0n, BRIDGED_GAS_BALANCE, BRIDGED_GAS_BALANCE], ); }); it('should allow Alice to subscribe by paying privately with bananas', async () => { - // Authorize the subscription contract to transfer the subscription amount from the subscriber. - await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet)); - expect(await bananaCoin.methods.balance_of_private(aliceAddress).view()).toBe( - BRIDGED_GAS_BALANCE - SUBSCRIPTION_AMOUNT - FEE_AMOUNT, + /** + PRIVATE SETUP + we first unshield `MAX_FEE` BC from alice's private balance to the FPC's public balance + + PUBLIC APP LOGIC + we then privately transfer `SUBSCRIPTION_AMOUNT` BC from alice to bob's subscription contract + + PUBLIC TEARDOWN + then the FPC calls `pay_fee`, reducing its gas balance by `FEE_AMOUNT`, and increasing the sequencer's gas balance by `FEE_AMOUNT` + the FPC also publicly sends `REFUND` BC to alice + */ + + await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), MAX_FEE); + + await expectMapping( + bananasPrivateBalances, + [aliceAddress, bobAddress, bananaFPC.address], + [PRIVATELY_MINTED_BANANAS - SUBSCRIPTION_AMOUNT - MAX_FEE, SUBSCRIPTION_AMOUNT, 0n], + ); + + await expectMapping( + bananasPublicBalances, + [aliceAddress, bobAddress, bananaFPC.address], + [PUBLICLY_MINTED_BANANAS + REFUND, 0n, FEE_AMOUNT], // alice receives a public refund (for now) ); - expect(await bananaCoin.methods.balance_of_private(bobAddress).view()).toBe(SUBSCRIPTION_AMOUNT); - expect(await bananaCoin.methods.balance_of_public(bananaFPC).view()).toBe(FEE_AMOUNT); - // remains unchanged - await assertPublicBalances( + await expectMapping( gasBalances, - [subscriptionContract.address, bananaFPC.address, sequencerAddress], - [BRIDGED_GAS_BALANCE, BRIDGED_GAS_BALANCE - FEE_AMOUNT, FEE_AMOUNT], + // note the subscription contract hasn't paid any fees yet + [bananaFPC.address, subscriptionContract.address, sequencerAddress], + [BRIDGED_GAS_BALANCE - FEE_AMOUNT, BRIDGED_GAS_BALANCE, FEE_AMOUNT], ); }); it('should allow Alice to subscribe by paying with bananas in public', async () => { - // Authorize the subscription contract to transfer the subscription amount from the subscriber. - await subscribe(new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet)); - - // assert that Alice paid 100n for the subscription - expect(await bananaCoin.methods.balance_of_private(aliceAddress).view()).toBe( - BRIDGED_GAS_BALANCE - 2n * SUBSCRIPTION_AMOUNT - FEE_AMOUNT, + /** + PRIVATE SETUP + we publicly transfer `MAX_FEE` BC from alice's public balance to the FPC's public balance + + PUBLIC APP LOGIC + we then privately transfer `SUBSCRIPTION_AMOUNT` BC from alice to bob's subscription contract + + PUBLIC TEARDOWN + then the FPC calls `pay_fee`, reducing its gas balance by `FEE_AMOUNT`, and increasing the sequencer's gas balance by `FEE_AMOUNT` + the FPC also publicly sends `REFUND` BC to alice + */ + await subscribe(new PublicFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), MAX_FEE); + + await expectMapping( + bananasPrivateBalances, + [aliceAddress, bobAddress, bananaFPC.address], + // we pay the fee publicly, but the subscription payment is still private. + // Also, minus 1 x MAX_FEE as leftover from the previous test, since we paid publicly this time + [PRIVATELY_MINTED_BANANAS - 2n * SUBSCRIPTION_AMOUNT - MAX_FEE, 2n * SUBSCRIPTION_AMOUNT, 0n], ); - expect(await bananaCoin.methods.balance_of_private(bobAddress).view()).toBe(2n * SUBSCRIPTION_AMOUNT); - // assert that Alice has paid one banana publicly for the tx above - expect(await bananaCoin.methods.balance_of_public(aliceAddress).view()).toBe(MINTED_BANANAS - FEE_AMOUNT); - expect(await bananaCoin.methods.balance_of_public(bananaFPC).view()).toBe(2n * FEE_AMOUNT); + await expectMapping( + bananasPublicBalances, + [aliceAddress, bobAddress, bananaFPC.address], + [ + // we have the refund from the previous test, + // but since we paid publicly this time, the refund should have been "squashed" + PUBLICLY_MINTED_BANANAS + REFUND - FEE_AMOUNT, + 0n, // Bob still has no public bananas + 2n * FEE_AMOUNT, // because this is the second time we've used the FPC + ], + ); - await assertPublicBalances( + await expectMapping( gasBalances, [subscriptionContract.address, bananaFPC.address, sequencerAddress], [BRIDGED_GAS_BALANCE, BRIDGED_GAS_BALANCE - 2n * FEE_AMOUNT, 2n * FEE_AMOUNT], @@ -166,16 +207,16 @@ describe('e2e_fees', () => { expect(await counterContract.methods.get_counter(bobAddress).view()).toBe(1n); - await assertPublicBalances( + await expectMapping( gasBalances, - [subscriptionContract.address, sequencerAddress], - [BRIDGED_GAS_BALANCE - FEE_AMOUNT, FEE_AMOUNT * 3n], + [subscriptionContract.address, bananaFPC.address, sequencerAddress], + [BRIDGED_GAS_BALANCE - FEE_AMOUNT, BRIDGED_GAS_BALANCE - 2n * FEE_AMOUNT, FEE_AMOUNT * 3n], ); }); it('should reject after the sub runs out', async () => { // subscribe again. This will overwrite the subscription - await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), 0); + await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), MAX_FEE, 0); await expect(dappIncrement()).rejects.toThrow( "Failed to solve brillig function, reason: explicit trap hit in brillig '(context.block_number()) as u64 < expiry_block_number as u64'", ); @@ -183,29 +224,32 @@ describe('e2e_fees', () => { it('should reject after the txs run out', async () => { // subscribe again. This will overwrite the subscription - await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), 5, 1); + await subscribe(new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, aliceWallet), FEE_AMOUNT, 5, 1); await expect(dappIncrement()).resolves.toBeDefined(); await expect(dappIncrement()).rejects.toThrow(/note.remaining_txs as u64 > 0/); }); - async function subscribe(paymentMethod: FeePaymentMethod, blockDelta: number = 5, txCount: number = 4) { - { - const nonce = Fr.random(); - const action = bananaCoin.methods.transfer(aliceAddress, bobAddress, SUBSCRIPTION_AMOUNT, nonce); - const messageHash = computeAuthWitMessageHash(subscriptionContract.address, action.request()); - await aliceWallet.createAuthWitness(messageHash); - - return subscriptionContract - .withWallet(aliceWallet) - .methods.subscribe(aliceAddress, nonce, (await e2eContext.pxe.getBlockNumber()) + blockDelta, txCount) - .send({ - fee: { - maxFee: 1n, - paymentMethod, - }, - }) - .wait(); - } + async function subscribe( + paymentMethod: FeePaymentMethod, + maxFee: bigint, + blockDelta: number = 5, + txCount: number = 4, + ) { + const nonce = Fr.random(); + const action = bananaCoin.methods.transfer(aliceAddress, bobAddress, SUBSCRIPTION_AMOUNT, nonce); + const messageHash = computeAuthWitMessageHash(subscriptionContract.address, action.request()); + await aliceWallet.createAuthWitness(messageHash); + + return subscriptionContract + .withWallet(aliceWallet) + .methods.subscribe(aliceAddress, nonce, (await e2eContext.pxe.getBlockNumber()) + blockDelta, txCount) + .send({ + fee: { + maxFee, + paymentMethod, + }, + }) + .wait(); } async function dappIncrement() { diff --git a/yarn-project/end-to-end/src/e2e_fees.test.ts b/yarn-project/end-to-end/src/e2e_fees.test.ts index 3ad0a36af47..d6e25bb8a84 100644 --- a/yarn-project/end-to-end/src/e2e_fees.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees.test.ts @@ -6,7 +6,6 @@ import { Note, PrivateFeePaymentMethod, TxHash, - computeAuthWitMessageHash, computeMessageSecretHash, } from '@aztec/aztec.js'; import { decodeFunctionSignature } from '@aztec/foundation/abi'; @@ -14,17 +13,11 @@ import { TokenContract as BananaCoin, FPCContract, GasTokenContract } from '@azt import { jest } from '@jest/globals'; -import { - EndToEndContext, - PublicBalancesFn, - assertPublicBalances, - getPublicBalancesFn, - setup, -} from './fixtures/utils.js'; -import { GasBridgingTestHarness } from './shared/gas_portal_test_harness.js'; +import { BalancesFn, EndToEndContext, expectMapping, getBalancesFn, setup } from './fixtures/utils.js'; +import { GasPortalTestingHarnessFactory, IGasBridgingTestHarness } from './shared/gas_portal_test_harness.js'; const TOKEN_NAME = 'BananaCoin'; -const TOKEN_SYMBOL = 'BAC'; +const TOKEN_SYMBOL = 'BC'; const TOKEN_DECIMALS = 18n; jest.setTimeout(100_000); @@ -36,11 +29,12 @@ describe('e2e_fees', () => { let bananaCoin: BananaCoin; let bananaFPC: FPCContract; - let gasBridgeTestHarness: GasBridgingTestHarness; + let gasBridgeTestHarness: IGasBridgingTestHarness; let e2eContext: EndToEndContext; - let gasBalances: PublicBalancesFn; - let bananaBalances: PublicBalancesFn; + let gasBalances: BalancesFn; + let bananaPublicBalances: BalancesFn; + let bananaPrivateBalances: BalancesFn; beforeAll(async () => { process.env.PXE_URL = ''; @@ -48,13 +42,14 @@ describe('e2e_fees', () => { const { wallets, accounts, logger, aztecNode, pxe, deployL1ContractsValues } = e2eContext; - gasBridgeTestHarness = await GasBridgingTestHarness.new( - pxe, - deployL1ContractsValues.publicClient, - deployL1ContractsValues.walletClient, - wallets[0], + gasBridgeTestHarness = await GasPortalTestingHarnessFactory.create({ + pxeService: pxe, + publicClient: deployL1ContractsValues.publicClient, + walletClient: deployL1ContractsValues.walletClient, + wallet: wallets[0], logger, - ); + mockL1: false, + }); gasTokenContract = gasBridgeTestHarness.l2Token; @@ -99,20 +94,20 @@ describe('e2e_fees', () => { e2eContext.logger(`bananaPay deployed at ${bananaFPC.address}`); await gasBridgeTestHarness.bridgeFromL1ToL2(InitialFPCGas + 1n, InitialFPCGas, bananaFPC.address); - gasBalances = getPublicBalancesFn('⛽', gasTokenContract, e2eContext.logger); - bananaBalances = getPublicBalancesFn('🍌', bananaCoin, e2eContext.logger); - await assertPublicBalances( - gasBalances, - [sequencerAddress, aliceAddress, bananaFPC.address], - [0n, 0n, InitialFPCGas], - ); - await assertPublicBalances(bananaBalances, [sequencerAddress, aliceAddress, bananaFPC.address], [0n, 0n, 0n]); + gasBalances = getBalancesFn('⛽', gasTokenContract.methods.balance_of_public, e2eContext.logger); + bananaPublicBalances = getBalancesFn('🍌.public', bananaCoin.methods.balance_of_public, e2eContext.logger); + bananaPrivateBalances = getBalancesFn('🍌.private', bananaCoin.methods.balance_of_private, e2eContext.logger); + await expectMapping(bananaPrivateBalances, [aliceAddress, bananaFPC.address, sequencerAddress], [0n, 0n, 0n]); + await expectMapping(bananaPublicBalances, [aliceAddress, bananaFPC.address, sequencerAddress], [0n, 0n, 0n]); + await expectMapping(gasBalances, [aliceAddress, bananaFPC.address, sequencerAddress], [0n, InitialFPCGas, 0n]); }, 100_000); it('mint banana privately, pay privately with banana via FPC', async () => { const PrivateInitialBananasAmount = 100n; const MintedBananasAmount = 1000n; const FeeAmount = 1n; + const RefundAmount = 2n; + const MaxFee = FeeAmount + RefundAmount; const { wallets, accounts } = e2eContext; // Mint bananas privately @@ -127,34 +122,61 @@ describe('e2e_fees', () => { const { visibleNotes } = receiptClaim.debugInfo!; expect(visibleNotes[0].note.items[0].toBigInt()).toBe(PrivateInitialBananasAmount); - // set up auth wit for FPC for to unshield Alice's bananas to itself - const nonce = 1; - const messageHash = computeAuthWitMessageHash( - bananaFPC.address, - bananaCoin.methods.unshield(accounts[0].address, bananaFPC.address, FeeAmount, nonce).request(), + await expectMapping( + bananaPrivateBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [PrivateInitialBananasAmount, 0n, 0n], ); - await wallets[0].createAuthWitness(messageHash); - + await expectMapping(bananaPublicBalances, [aliceAddress, bananaFPC.address, sequencerAddress], [0n, 0n, 0n]); + await expectMapping(gasBalances, [aliceAddress, bananaFPC.address, sequencerAddress], [0n, InitialFPCGas, 0n]); + + /** + * PRIVATE SETUP + * check authwit + * reduce alice BC.private by MaxFee + * enqueue public call to increase FPC BC.public by MaxFee + * enqueue public call for fpc.pay_fee + * + * PUBLIC SETUP + * increase FPC BC.public by MaxFee + * + * PUBLIC APP LOGIC + * increase alice BC.public by MintedBananasAmount + * increase BC total supply by MintedBananasAmount + * + * PUBLIC TEARDOWN + * call gas.pay_fee + * decrease FPC AZT by FeeAmount + * increase sequencer AZT by FeeAmount + * call banana.transfer_public + * decrease FPC BC.public by RefundAmount + * increase alice BC.public by RefundAmount + * + */ await bananaCoin.methods .mint_public(aliceAddress, MintedBananasAmount) .send({ fee: { - maxFee: FeeAmount, + maxFee: MaxFee, paymentMethod: new PrivateFeePaymentMethod(bananaCoin.address, bananaFPC.address, wallets[0]), }, }) .wait(); - await assertPublicBalances( - gasBalances, - [sequencerAddress, aliceAddress, bananaFPC.address], - [FeeAmount, 0n, InitialFPCGas - FeeAmount], + await expectMapping( + bananaPrivateBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [PrivateInitialBananasAmount - MaxFee, 0n, 0n], ); - - await assertPublicBalances( - bananaBalances, - [sequencerAddress, aliceAddress, bananaFPC.address], - [0n, MintedBananasAmount, FeeAmount], + await expectMapping( + bananaPublicBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [MintedBananasAmount + RefundAmount, MaxFee - RefundAmount, 0n], + ); + await expectMapping( + gasBalances, + [aliceAddress, bananaFPC.address, sequencerAddress], + [0n, InitialFPCGas - FeeAmount, FeeAmount], ); }, 100_000); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 534963a9ab1..c8a118bee3e 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -8,7 +8,7 @@ import { BatchCall, CheatCodes, CompleteAddress, - Contract, + ContractMethod, DebugLogger, DeployL1Contracts, EthCheatCodes, @@ -431,14 +431,14 @@ export const expectUnencryptedLogsFromLastBlockToBe = async (pxe: PXE, logMessag expect(asciiLogs).toStrictEqual(logMessages); }; -export type PublicBalancesFn = ReturnType; -export function getPublicBalancesFn( +export type BalancesFn = ReturnType; +export function getBalancesFn( symbol: string, - contract: Contract, + method: ContractMethod, logger: any, ): (...addresses: AztecAddress[]) => Promise { const balances = async (...addresses: AztecAddress[]) => { - const b = await Promise.all(addresses.map(address => contract.methods.balance_of_public(address).view())); + const b = await Promise.all(addresses.map(address => method(address).view())); const debugString = `${symbol} balances: ${addresses.map((address, i) => `${address}: ${b[i]}`).join(', ')}`; logger(debugString); return b; @@ -447,13 +447,14 @@ export function getPublicBalancesFn( return balances; } -export async function assertPublicBalances( - balances: PublicBalancesFn, - addresses: AztecAddress[], - expectedBalances: bigint[], -) { - const actualBalances = await balances(...addresses); - for (let i = 0; i < addresses.length; i++) { - expect(actualBalances[i]).toBe(expectedBalances[i]); - } +export async function expectMapping( + fn: (...k: K[]) => Promise, + inputs: K[], + expectedOutputs: V[], +): Promise { + expect(inputs.length).toBe(expectedOutputs.length); + + const outputs = await fn(...inputs); + + expect(outputs).toEqual(expectedOutputs); } diff --git a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts index 6f6aea67170..4f34d8242ec 100644 --- a/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts +++ b/yarn-project/end-to-end/src/shared/gas_portal_test_harness.ts @@ -1,4 +1,3 @@ -// docs:start:cross_chain_test_harness import { AztecAddress, DebugLogger, @@ -17,7 +16,11 @@ import { getCanonicalGasToken } from '@aztec/protocol-contracts/gas-token'; import { Account, Chain, HttpTransport, PublicClient, WalletClient, getContract } from 'viem'; -// docs:start:deployAndInitializeTokenAndBridgeContracts +export interface IGasBridgingTestHarness { + bridgeFromL1ToL2(l1TokenBalance: bigint, bridgeAmount: bigint, owner: AztecAddress): Promise; + l2Token: GasTokenContract; +} + /** * Deploy L1 token and portal, initialize portal, deploy a non native l2 token contract, its L2 bridge contract and attach is to the portal. * @param wallet - the wallet instance @@ -83,21 +86,33 @@ export async function deployAndInitializeTokenAndBridgeContracts( return { gasL2, gasPortalAddress, gasPortal, gasL1 }; } -// docs:end:deployAndInitializeTokenAndBridgeContracts -/** - * A Class for testing cross chain interactions, contains common interactions - * shared between cross chain tests. - */ -export class GasBridgingTestHarness { - static async new( - pxeService: PXE, - publicClient: PublicClient, - walletClient: any, - wallet: Wallet, - logger: DebugLogger, - underlyingERC20Address?: EthAddress, - ): Promise { +export interface GasPortalTestingHarnessFactoryConfig { + pxeService: PXE; + publicClient: PublicClient; + walletClient: WalletClient; + wallet: Wallet; + logger: DebugLogger; + underlyingERC20Address?: EthAddress; + mockL1?: boolean; +} +export class GasPortalTestingHarnessFactory { + private constructor(private config: GasPortalTestingHarnessFactoryConfig) {} + + private async createMock() { + const wallet = this.config.wallet; + + const gasL2 = await GasTokenContract.deploy(wallet) + .send({ + contractAddressSalt: getCanonicalGasToken().instance.salt, + }) + .deployed(); + return Promise.resolve(new MockGasBridgingTestHarness(gasL2)); + } + + private async createReal() { + const { pxeService, publicClient, walletClient, wallet, logger, underlyingERC20Address } = this.config; + const ethAccount = EthAddress.fromString((await walletClient.getAddresses())[0]); const owner = wallet.getCompleteAddress(); const l1ContractAddresses = (await pxeService.getNodeInfo()).l1ContractAddresses; @@ -134,6 +149,28 @@ export class GasBridgingTestHarness { ); } + static create(config: GasPortalTestingHarnessFactoryConfig): Promise { + const factory = new GasPortalTestingHarnessFactory(config); + if (config.mockL1) { + return factory.createMock(); + } else { + return factory.createReal(); + } + } +} + +class MockGasBridgingTestHarness implements IGasBridgingTestHarness { + constructor(public l2Token: GasTokenContract) {} + async bridgeFromL1ToL2(_l1TokenBalance: bigint, bridgeAmount: bigint, owner: AztecAddress): Promise { + await this.l2Token.methods.mint_public(owner, bridgeAmount).send().wait(); + } +} + +/** + * A Class for testing cross chain interactions, contains common interactions + * shared between cross chain tests. + */ +class GasBridgingTestHarness implements IGasBridgingTestHarness { constructor( /** Private eXecution Environment (PXE). */ public pxeService: PXE, diff --git a/yarn-project/foundation/src/array/array.ts b/yarn-project/foundation/src/array/array.ts index 274ad4bf36f..59ad84b30c6 100644 --- a/yarn-project/foundation/src/array/array.ts +++ b/yarn-project/foundation/src/array/array.ts @@ -84,3 +84,36 @@ export function assertItemsLength< } } } + +/** + * Checks that the permutation is valid. Throws an error if it is not. + * @param original - The original array. + * @param permutation - The array which is allegedly a permutation of the original. + * @param indexes - The indices of the original array which the permutation should map to. + * @param isEqual - A function to compare the elements of the original and permutation arrays. + */ +export function assertPermutation( + original: T[], + permutation: T[], + indexes: number[], + isEqual: (a: T, b: T) => boolean, +): void { + if (original.length !== permutation.length || original.length !== indexes.length) { + throw new Error(`Invalid lengths: ${original.length}, ${permutation.length}, ${indexes.length}`); + } + + const seenValue = new Set(); + for (let i = 0; i < indexes.length; i++) { + const index = indexes[i]; + const permutedValue = permutation[i]; + const originalValueAtIndex = original[index]; + + if (!isEqual(permutedValue, originalValueAtIndex)) { + throw new Error(`Invalid permutation at index ${index}: ${permutedValue} !== ${originalValueAtIndex}`); + } + if (seenValue.has(index)) { + throw new Error(`Duplicate index in permutation: ${index}`); + } + seenValue.add(index); + } +} diff --git a/yarn-project/noir-contracts.js/tsconfig.json b/yarn-project/noir-contracts.js/tsconfig.json index 279682403a1..5673a9b2440 100644 --- a/yarn-project/noir-contracts.js/tsconfig.json +++ b/yarn-project/noir-contracts.js/tsconfig.json @@ -10,9 +10,6 @@ "path": "../aztec.js" } ], - "include": [ - "src", - "artifacts", - "artifacts/*.json", - ], -} \ No newline at end of file + "include": ["src", "artifacts", "artifacts/*.json"], + "exclude": ["dest"] +} diff --git a/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts b/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts index fd3eea9f660..ff701339680 100644 --- a/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts +++ b/yarn-project/noir-protocol-circuits-types/src/type_conversion.ts @@ -87,6 +87,8 @@ import { ReadRequestContext, ReadRequestMembershipWitness, ReadRequestStatus, + RollupKernelCircuitPublicInputs, + RollupKernelData, RootRollupInputs, RootRollupPublicInputs, SettledReadHint, @@ -170,6 +172,8 @@ import { PublicDataMembershipWitness as PublicDataMembershipWitnessNoir, PublicDataTreeLeaf as PublicDataTreeLeafNoir, PublicDataTreeLeafPreimage as PublicDataTreeLeafPreimageNoir, + RollupKernelCircuitPublicInputs as RollupKernelCircuitPublicInputsNoir, + RollupKernelData as RollupKernelDataNoir, StateDiffHints as StateDiffHintsNoir, } from './types/rollup_base_types.js'; import { MergeRollupInputs as MergeRollupInputsNoir } from './types/rollup_merge_types.js'; @@ -1184,6 +1188,16 @@ export function mapPublicKernelCircuitPublicInputsToNoir( }; } +export function mapRollupKernelCircuitPublicInputsToNoir( + inputs: RollupKernelCircuitPublicInputs, +): RollupKernelCircuitPublicInputsNoir { + return { + aggregation_object: {}, + constants: mapCombinedConstantDataToNoir(inputs.constants), + end: mapCombinedAccumulatedDataToNoir(inputs.end), + }; +} + export function mapPublicAccumulatedRevertibleDataToNoir( data: PublicAccumulatedRevertibleData, ): PublicAccumulatedRevertibleDataNoir { @@ -1236,6 +1250,16 @@ export function mapPublicKernelDataToNoir(publicKernelData: PublicKernelData): P }; } +export function mapRollupKernelDataToNoir(rollupKernelData: RollupKernelData): RollupKernelDataNoir { + return { + public_inputs: mapRollupKernelCircuitPublicInputsToNoir(rollupKernelData.publicInputs), + proof: {}, + vk: {}, + vk_index: mapFieldToNoir(new Fr(rollupKernelData.vkIndex)), + vk_path: mapTuple(rollupKernelData.vkPath, mapFieldToNoir), + }; +} + export function mapPrivateKernelInnerCircuitPublicInputsFromNoir( inputs: PrivateKernelInnerCircuitPublicInputsNoir, ): PrivateKernelInnerCircuitPublicInputs { @@ -1924,7 +1948,7 @@ export function mapStateDiffHintsToNoir(hints: StateDiffHints): StateDiffHintsNo */ export function mapBaseRollupInputsToNoir(inputs: BaseRollupInputs): BaseRollupInputsNoir { return { - kernel_data: mapPublicKernelDataToNoir(inputs.kernelData), + kernel_data: mapRollupKernelDataToNoir(inputs.kernelData), start: mapPartialStateReferenceToNoir(inputs.start), state_diff_hints: mapStateDiffHintsToNoir(inputs.stateDiffHints), diff --git a/yarn-project/sequencer-client/src/block_builder/solo_block_builder.ts b/yarn-project/sequencer-client/src/block_builder/solo_block_builder.ts index 211c5856c40..0e0230791cf 100644 --- a/yarn-project/sequencer-client/src/block_builder/solo_block_builder.ts +++ b/yarn-project/sequencer-client/src/block_builder/solo_block_builder.ts @@ -6,6 +6,7 @@ import { BaseRollupInputs, CONTRACT_SUBTREE_HEIGHT, CONTRACT_SUBTREE_SIBLING_PATH_LENGTH, + CombinedAccumulatedData, ConstantRollupData, GlobalVariables, L1_TO_L2_MSG_SUBTREE_HEIGHT, @@ -32,8 +33,9 @@ import { Proof, PublicDataTreeLeaf, PublicDataTreeLeafPreimage, - PublicKernelData, ROLLUP_VK_TREE_HEIGHT, + RollupKernelCircuitPublicInputs, + RollupKernelData, RollupTypes, RootRollupInputs, RootRollupPublicInputs, @@ -44,7 +46,7 @@ import { VK_TREE_HEIGHT, VerificationKey, } from '@aztec/circuits.js'; -import { makeTuple } from '@aztec/foundation/array'; +import { assertPermutation, makeTuple } from '@aztec/foundation/array'; import { toBigIntBE } from '@aztec/foundation/bigint-buffer'; import { padArrayEnd } from '@aztec/foundation/collection'; import { Fr } from '@aztec/foundation/fields'; @@ -416,9 +418,14 @@ export class SoloBlockBuilder implements BlockBuilder { ); } - protected getKernelDataFor(tx: ProcessedTx) { - return new PublicKernelData( - tx.data, + protected getKernelDataFor(tx: ProcessedTx): RollupKernelData { + const inputs = new RollupKernelCircuitPublicInputs( + tx.data.aggregationObject, + CombinedAccumulatedData.recombine(tx.data.endNonRevertibleData, tx.data.end), + tx.data.constants, + ); + return new RollupKernelData( + inputs, tx.proof, // VK for the kernel circuit @@ -500,12 +507,12 @@ export class SoloBlockBuilder implements BlockBuilder { protected async processPublicDataUpdateRequests(tx: ProcessedTx) { const combinedPublicDataUpdateRequests = tx.data.combinedData.publicDataUpdateRequests.map(updateRequest => { - return new PublicDataTreeLeaf(updateRequest.leafSlot, updateRequest.newValue).toBuffer(); + return new PublicDataTreeLeaf(updateRequest.leafSlot, updateRequest.newValue); }); const { lowLeavesWitnessData, newSubtreeSiblingPath, sortedNewLeaves, sortedNewLeavesIndexes } = await this.db.batchInsert( MerkleTreeId.PUBLIC_DATA_TREE, - combinedPublicDataUpdateRequests, + combinedPublicDataUpdateRequests.map(x => x.toBuffer()), // TODO(#3675) remove oldValue from update requests PUBLIC_DATA_SUBTREE_HEIGHT, ); @@ -545,6 +552,12 @@ export class SoloBlockBuilder implements BlockBuilder { return lowLeavesWitnessData[i].leafPreimage as PublicDataTreeLeafPreimage; }); + // validate that the sortedPublicDataWrites and sortedPublicDataWritesIndexes are in the correct order + // otherwise it will just fail in the circuit + assertPermutation(combinedPublicDataUpdateRequests, sortedPublicDataWrites, sortedPublicDataWritesIndexes, (a, b) => + a.equals(b), + ); + return { lowPublicDataWritesPreimages, lowPublicDataWritesMembershipWitnesses, diff --git a/yarn-project/sequencer-client/src/sequencer/abstract_phase_manager.ts b/yarn-project/sequencer-client/src/sequencer/abstract_phase_manager.ts index 33fd74e3669..b8e9960d1a0 100644 --- a/yarn-project/sequencer-client/src/sequencer/abstract_phase_manager.ts +++ b/yarn-project/sequencer-client/src/sequencer/abstract_phase_manager.ts @@ -40,7 +40,7 @@ import { import { computeVarArgsHash } from '@aztec/circuits.js/hash'; import { arrayNonEmptyLength, padArrayEnd } from '@aztec/foundation/collection'; import { DebugLogger, createDebugLogger } from '@aztec/foundation/log'; -import { to2Fields } from '@aztec/foundation/serialize'; +import { Tuple, to2Fields } from '@aztec/foundation/serialize'; import { PublicExecution, PublicExecutionResult, @@ -64,6 +64,12 @@ export enum PublicKernelPhase { TEARDOWN = 'teardown', } +export const PhaseIsRevertible: Record = { + [PublicKernelPhase.SETUP]: false, + [PublicKernelPhase.APP_LOGIC]: true, + [PublicKernelPhase.TEARDOWN]: false, +}; + export abstract class AbstractPhaseManager { protected log: DebugLogger; constructor( @@ -243,11 +249,11 @@ export abstract class AbstractPhaseManager { } // HACK(#1622): Manually patches the ordering of public state actions // TODO(#757): Enforce proper ordering of public state actions - this.patchPublicStorageActionOrdering(kernelOutput, enqueuedExecutionResult!); + patchPublicStorageActionOrdering(kernelOutput, enqueuedExecutionResult!, this.phase); } // TODO(#3675): This should be done in a public kernel circuit - this.removeRedundantPublicDataWrites(kernelOutput); + removeRedundantPublicDataWrites(kernelOutput); return [kernelOutput, kernelProof, newUnencryptedFunctionLogs]; } @@ -377,165 +383,107 @@ export abstract class AbstractPhaseManager { const proof = await this.publicProver.getPublicCircuitProof(callStackItem.publicInputs); return new PublicCallData(callStackItem, publicCallStack, proof, portalContractAddress, bytecodeHash); } +} - // HACK(#1622): this is a hack to fix ordering of public state in the call stack. Since the private kernel - // cannot keep track of side effects that happen after or before a nested call, we override the public - // state actions it emits with whatever we got from the simulator. As a sanity check, we at least verify - // that the elements are the same, so we are only tweaking their ordering. - // See yarn-project/end-to-end/src/e2e_ordering.test.ts - // See https://github.com/AztecProtocol/aztec-packages/issues/1616 - // TODO(#757): Enforce proper ordering of public state actions - /** - * Patch the ordering of storage actions output from the public kernel. - * @param publicInputs - to be patched here: public inputs to the kernel iteration up to this point - * @param execResult - result of the top/first execution for this enqueued public call - */ - private patchPublicStorageActionOrdering( - publicInputs: PublicKernelCircuitPublicInputs, - execResult: PublicExecutionResult, - ) { - const { publicDataReads: revertiblePublicDataReads, publicDataUpdateRequests: revertiblePublicDataUpdateRequests } = - publicInputs.end; // from kernel - const { - publicDataReads: nonRevertiblePublicDataReads, - publicDataUpdateRequests: nonRevertiblePublicDataUpdateRequests, - } = publicInputs.endNonRevertibleData; // from kernel - - // Convert ContractStorage* objects to PublicData* objects and sort them in execution order - const simPublicDataReads = collectPublicDataReads(execResult); - const simPublicDataUpdateRequests = collectPublicDataUpdateRequests(execResult); - - const simRevertiblePublicDataReads = simPublicDataReads.filter(read => - revertiblePublicDataReads.find(item => item.leafSlot.equals(read.leafSlot) && item.value.equals(read.value)), - ); - const simRevertiblePublicDataUpdateRequests = simPublicDataUpdateRequests.filter(update => - revertiblePublicDataUpdateRequests.find( - item => item.leafSlot.equals(update.leafSlot) && item.newValue.equals(update.newValue), - ), - ); - - const simNonRevertiblePublicDataReads = simPublicDataReads.filter(read => - nonRevertiblePublicDataReads.find(item => item.leafSlot.equals(read.leafSlot) && item.value.equals(read.value)), - ); - const simNonRevertiblePublicDataUpdateRequests = simPublicDataUpdateRequests.filter(update => - nonRevertiblePublicDataUpdateRequests.find( - item => item.leafSlot.equals(update.leafSlot) && item.newValue.equals(update.newValue), - ), - ); - - // Assume that kernel public inputs has the right number of items. - // We only want to reorder the items from the public inputs of the - // most recently processed top/enqueued call. - const numRevertibleReadsInKernel = arrayNonEmptyLength(publicInputs.end.publicDataReads, f => f.isEmpty()); - const numRevertibleUpdatesInKernel = arrayNonEmptyLength(publicInputs.end.publicDataUpdateRequests, f => - f.isEmpty(), - ); - const numNonRevertibleReadsInKernel = arrayNonEmptyLength(publicInputs.endNonRevertibleData.publicDataReads, f => - f.isEmpty(), - ); - const numNonRevertibleUpdatesInKernel = arrayNonEmptyLength( - publicInputs.endNonRevertibleData.publicDataUpdateRequests, - f => f.isEmpty(), - ); - - // Validate all items in enqueued public calls are in the kernel emitted stack - const readsAreEqual = - simRevertiblePublicDataReads.length + simNonRevertiblePublicDataReads.length === simPublicDataReads.length; - - const updatesAreEqual = - simRevertiblePublicDataUpdateRequests.length + simNonRevertiblePublicDataUpdateRequests.length === - simPublicDataUpdateRequests.length; +function removeRedundantPublicDataWrites(publicInputs: PublicKernelCircuitPublicInputs) { + const patch = (requests: Tuple) => { + const lastWritesMap = new Map(); + for (const write of requests) { + const key = write.leafSlot.toString(); + lastWritesMap.set(key, write); + } + return requests.filter(write => lastWritesMap.get(write.leafSlot.toString())?.equals(write)); + }; + + publicInputs.end.publicDataUpdateRequests = padArrayEnd( + patch(publicInputs.end.publicDataUpdateRequests), + PublicDataUpdateRequest.empty(), + MAX_REVERTIBLE_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + ); + + publicInputs.endNonRevertibleData.publicDataUpdateRequests = padArrayEnd( + patch(publicInputs.endNonRevertibleData.publicDataUpdateRequests), + PublicDataUpdateRequest.empty(), + MAX_NON_REVERTIBLE_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + ); +} - if (!readsAreEqual) { +// HACK(#1622): this is a hack to fix ordering of public state in the call stack. Since the private kernel +// cannot keep track of side effects that happen after or before a nested call, we override the public +// state actions it emits with whatever we got from the simulator. As a sanity check, we at least verify +// that the elements are the same, so we are only tweaking their ordering. +// See yarn-project/end-to-end/src/e2e_ordering.test.ts +// See https://github.com/AztecProtocol/aztec-packages/issues/1616 +// TODO(#757): Enforce proper ordering of public state actions +/** + * Patch the ordering of storage actions output from the public kernel. + * @param publicInputs - to be patched here: public inputs to the kernel iteration up to this point + * @param execResult - result of the top/first execution for this enqueued public call + */ +function patchPublicStorageActionOrdering( + publicInputs: PublicKernelCircuitPublicInputs, + execResult: PublicExecutionResult, + phase: PublicKernelPhase, +) { + const { publicDataReads, publicDataUpdateRequests } = PhaseIsRevertible[phase] + ? publicInputs.end + : publicInputs.endNonRevertibleData; + + // Convert ContractStorage* objects to PublicData* objects and sort them in execution order. + // Note, this only pulls simulated reads/writes from the current phase, + // so the returned result will be a subset of the public kernel output. + + const simPublicDataReads = collectPublicDataReads(execResult); + // verify that each read is in the kernel output + for (const read of simPublicDataReads) { + if (!publicDataReads.find(item => item.equals(read))) { throw new Error( `Public data reads from simulator do not match those from public kernel.\nFrom simulator: ${simPublicDataReads .map(p => p.toFriendlyJSON()) - .join(', ')}\nFrom public kernel revertible: ${revertiblePublicDataReads - .map(i => i.toFriendlyJSON()) - .join(', ')}\nFrom public kernel non-revertible: ${nonRevertiblePublicDataReads - .map(i => i.toFriendlyJSON()) - .join(', ')}`, + .join(', ')}\nFrom public kernel: ${publicDataReads.map(i => i.toFriendlyJSON()).join(', ')}`, ); } - if (!updatesAreEqual) { + } + + const simPublicDataUpdateRequests = collectPublicDataUpdateRequests(execResult); + for (const update of simPublicDataUpdateRequests) { + if (!publicDataUpdateRequests.find(item => item.equals(update))) { throw new Error( `Public data update requests from simulator do not match those from public kernel.\nFrom simulator: ${simPublicDataUpdateRequests .map(p => p.toFriendlyJSON()) - .join(', ')}\nFrom public kernel revertible: ${revertiblePublicDataUpdateRequests - .map(i => i.toFriendlyJSON()) - .join(', ')}\nFrom public kernel non-revertible: ${nonRevertiblePublicDataUpdateRequests + .join(', ')}\nFrom public kernel revertible: ${publicDataUpdateRequests .map(i => i.toFriendlyJSON()) .join(', ')}`, ); } - - const numRevertibleReadsBeforeThisEnqueuedCall = numRevertibleReadsInKernel - simRevertiblePublicDataReads.length; - const numRevertibleUpdatesBeforeThisEnqueuedCall = - numRevertibleUpdatesInKernel - simRevertiblePublicDataUpdateRequests.length; - - const numNonRevertibleReadsBeforeThisEnqueuedCall = - numNonRevertibleReadsInKernel - simNonRevertiblePublicDataReads.length; - const numNonRevertibleUpdatesBeforeThisEnqueuedCall = - numNonRevertibleUpdatesInKernel - simNonRevertiblePublicDataUpdateRequests.length; - - // Override revertible kernel output - publicInputs.end.publicDataReads = padArrayEnd( - [ - // do not mess with items from previous top/enqueued calls in kernel output - ...publicInputs.end.publicDataReads.slice(0, numRevertibleReadsBeforeThisEnqueuedCall), - ...simRevertiblePublicDataReads, - ], - PublicDataRead.empty(), - MAX_REVERTIBLE_PUBLIC_DATA_READS_PER_TX, - ); - - publicInputs.end.publicDataUpdateRequests = padArrayEnd( - [ - ...publicInputs.end.publicDataUpdateRequests.slice(0, numRevertibleUpdatesBeforeThisEnqueuedCall), - ...simRevertiblePublicDataUpdateRequests, - ], - PublicDataUpdateRequest.empty(), - MAX_REVERTIBLE_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, - ); - - publicInputs.endNonRevertibleData.publicDataReads = padArrayEnd( - [ - ...publicInputs.endNonRevertibleData.publicDataReads.slice(0, numNonRevertibleReadsBeforeThisEnqueuedCall), - ...simNonRevertiblePublicDataReads, - ], - PublicDataRead.empty(), - MAX_NON_REVERTIBLE_PUBLIC_DATA_READS_PER_TX, - ); - - publicInputs.endNonRevertibleData.publicDataUpdateRequests = padArrayEnd( - [ - ...publicInputs.endNonRevertibleData.publicDataUpdateRequests.slice( - 0, - numNonRevertibleUpdatesBeforeThisEnqueuedCall, - ), - ...simNonRevertiblePublicDataUpdateRequests, - ], - PublicDataUpdateRequest.empty(), - MAX_NON_REVERTIBLE_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, - ); - } - - private removeRedundantPublicDataWrites(publicInputs: PublicKernelCircuitPublicInputs) { - const lastWritesMap = new Map(); - for (const write of publicInputs.end.publicDataUpdateRequests) { - const key = write.leafSlot.toString(); - lastWritesMap.set(key, write); - } - - const lastWrites = publicInputs.end.publicDataUpdateRequests.filter(write => - lastWritesMap.get(write.leafSlot.toString())?.equals(write), - ); - - publicInputs.end.publicDataUpdateRequests = padArrayEnd( - lastWrites, - - PublicDataUpdateRequest.empty(), - MAX_REVERTIBLE_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, - ); } + // We only want to reorder the items from the public inputs of the + // most recently processed top/enqueued call. + + const effectSet = PhaseIsRevertible[phase] ? 'end' : 'endNonRevertibleData'; + + const numReadsInKernel = arrayNonEmptyLength(publicDataReads, f => f.isEmpty()); + const numReadsBeforeThisEnqueuedCall = numReadsInKernel - simPublicDataReads.length; + publicInputs[effectSet].publicDataReads = padArrayEnd( + [ + // do not mess with items from previous top/enqueued calls in kernel output + ...publicInputs[effectSet].publicDataReads.slice(0, numReadsBeforeThisEnqueuedCall), + ...simPublicDataReads, + ], + PublicDataRead.empty(), + PhaseIsRevertible[phase] ? MAX_REVERTIBLE_PUBLIC_DATA_READS_PER_TX : MAX_NON_REVERTIBLE_PUBLIC_DATA_READS_PER_TX, + ); + + const numUpdatesInKernel = arrayNonEmptyLength(publicDataUpdateRequests, f => f.isEmpty()); + const numUpdatesBeforeThisEnqueuedCall = numUpdatesInKernel - simPublicDataUpdateRequests.length; + publicInputs[effectSet].publicDataUpdateRequests = padArrayEnd( + [ + ...publicInputs[effectSet].publicDataUpdateRequests.slice(0, numUpdatesBeforeThisEnqueuedCall), + ...simPublicDataUpdateRequests, + ], + PublicDataUpdateRequest.empty(), + PhaseIsRevertible[phase] + ? MAX_REVERTIBLE_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX + : MAX_NON_REVERTIBLE_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX, + ); } diff --git a/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts b/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts index 866559e5182..d5013bed5ed 100644 --- a/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/public_processor.test.ts @@ -13,6 +13,7 @@ import { AztecAddress, CallContext, CallRequest, + ContractStorageUpdateRequest, EthAddress, Fr, FunctionData, @@ -26,20 +27,24 @@ import { PublicAccumulatedNonRevertibleData, PublicAccumulatedRevertibleData, PublicCallRequest, + PublicDataUpdateRequest, PublicKernelCircuitPublicInputs, makeEmptyProof, } from '@aztec/circuits.js'; +import { computePublicDataTreeLeafSlot } from '@aztec/circuits.js/hash'; import { + fr, makeAztecAddress, makePrivateKernelTailCircuitPublicInputs, makePublicCallRequest, makeSelector, } from '@aztec/circuits.js/testing'; import { makeTuple } from '@aztec/foundation/array'; -import { padArrayEnd, times } from '@aztec/foundation/collection'; +import { arrayNonEmptyLength, padArrayEnd, times } from '@aztec/foundation/collection'; import { PublicExecution, PublicExecutionResult, PublicExecutor } from '@aztec/simulator'; import { MerkleTreeOperations, TreeInfo } from '@aztec/world-state'; +import { jest } from '@jest/globals'; import { MockProxy, mock } from 'jest-mock-extended'; import { PublicProver } from '../prover/index.js'; @@ -319,7 +324,13 @@ describe('public_processor', () => { }); it('runs a tx with setup and teardown phases', async function () { - const callRequests: PublicCallRequest[] = [0x100, 0x200, 0x300].map(makePublicCallRequest); + const baseContractAddressSeed = 0x200; + const baseContractAddress = makeAztecAddress(baseContractAddressSeed); + const callRequests: PublicCallRequest[] = [ + baseContractAddressSeed, + baseContractAddressSeed, + baseContractAddressSeed, + ].map(makePublicCallRequest); callRequests[0].callContext.startSideEffectCounter = 2; callRequests[1].callContext.startSideEffectCounter = 3; callRequests[2].callContext.startSideEffectCounter = 4; @@ -342,29 +353,112 @@ describe('public_processor', () => { MAX_REVERTIBLE_PUBLIC_CALL_STACK_LENGTH_PER_TX, ); kernelOutput.end.privateCallStack = padArrayEnd([], CallRequest.empty(), MAX_PRIVATE_CALL_STACK_LENGTH_PER_TX); - callRequests.reverse(); - const tx = new Tx(kernelOutput, proof, TxL2Logs.random(2, 3), TxL2Logs.random(3, 2), callRequests, [ - ExtendedContractData.random(), - ]); + const tx = new Tx( + kernelOutput, + proof, + TxL2Logs.random(2, 3), + TxL2Logs.random(3, 2), + // reverse because `enqueuedPublicFunctions` expects the last element to be the front of the queue + callRequests.slice().reverse(), + [ExtendedContractData.random()], + ); + + // const enqueuedExecutionContractAddress = makeAztecAddress(30); + const enqueuedExecutionContractAddress = baseContractAddress; + const contractSlotA = fr(0x100); + const contractSlotB = fr(0x150); + const contractSlotC = fr(0x200); + + let simulatorCallCount = 0; publicExecutor.simulate.mockImplementation(execution => { - for (const request of callRequests) { - if (execution.contractAddress.equals(request.contractAddress)) { - return Promise.resolve(makePublicExecutionResultFromRequest(request)); - } + let executionResult: PublicExecutionResult; + + // first call is for setup + if (simulatorCallCount === 0) { + executionResult = makePublicExecutionResultFromRequest(callRequests[0]); } - throw new Error(`Unexpected execution request: ${execution}`); + // second call is for app logic + else if (simulatorCallCount === 1) { + // which is the call enqueued last chronologically + executionResult = makePublicExecutionResultFromRequest(callRequests[2]); + executionResult.contractStorageUpdateRequests = [ + new ContractStorageUpdateRequest(contractSlotA, fr(0x101)), + new ContractStorageUpdateRequest(contractSlotB, fr(0x151)), + ]; + } + // third call is for teardown + else if (simulatorCallCount === 2) { + // which is the call enqueued second chronologically + executionResult = makePublicExecutionResultFromRequest(callRequests[1]); + // if this is the call for teardown, enqueue additional call + executionResult.nestedExecutions = [ + makePublicExecutionResult( + executionResult.execution.contractAddress, + { + to: enqueuedExecutionContractAddress, + functionData: new FunctionData(makeSelector(5), false, false, false), + args: new Array(ARGS_LENGTH).fill(Fr.ZERO), + }, + [], + [ + new ContractStorageUpdateRequest(contractSlotA, fr(0x101)), + new ContractStorageUpdateRequest(contractSlotC, fr(0x201)), + ], + ), + makePublicExecutionResult( + executionResult.execution.contractAddress, + { + to: enqueuedExecutionContractAddress, + functionData: new FunctionData(makeSelector(6), false, false, false), + args: new Array(ARGS_LENGTH).fill(Fr.ZERO), + }, + [], + [new ContractStorageUpdateRequest(contractSlotA, fr(0x102))], + ), + ]; + } else { + throw new Error(`Unexpected execution request: ${execution}, call count: ${simulatorCallCount}`); + } + simulatorCallCount++; + return Promise.resolve(executionResult); }); + const setupSpy = jest.spyOn(publicKernel, 'publicKernelCircuitSetup'); + const appLogicSpy = jest.spyOn(publicKernel, 'publicKernelCircuitAppLogic'); + const teardownSpy = jest.spyOn(publicKernel, 'publicKernelCircuitTeardown'); + const [processed, failed] = await processor.process([tx]); expect(processed).toHaveLength(1); expect(processed).toEqual([expectedTxByHash(tx)]); expect(failed).toHaveLength(0); + + expect(setupSpy).toHaveBeenCalledTimes(1); + expect(appLogicSpy).toHaveBeenCalledTimes(1); + expect(teardownSpy).toHaveBeenCalledTimes(3); expect(publicExecutor.simulate).toHaveBeenCalledTimes(3); expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(3); expect(publicWorldStateDB.rollback).toHaveBeenCalledTimes(0); + expect( + arrayNonEmptyLength(processed[0].data.combinedData.publicDataUpdateRequests, PublicDataUpdateRequest.isEmpty), + ).toEqual(3); + expect(processed[0].data.combinedData.publicDataUpdateRequests[0]).toEqual( + new PublicDataUpdateRequest(computePublicDataTreeLeafSlot(baseContractAddress, contractSlotA), fr(0x102)), + ); + expect(processed[0].data.combinedData.publicDataUpdateRequests[1]).toEqual( + new PublicDataUpdateRequest( + computePublicDataTreeLeafSlot(enqueuedExecutionContractAddress, contractSlotB), + fr(0x151), + ), + ); + expect(processed[0].data.combinedData.publicDataUpdateRequests[2]).toEqual( + new PublicDataUpdateRequest( + computePublicDataTreeLeafSlot(enqueuedExecutionContractAddress, contractSlotC), + fr(0x201), + ), + ); }); }); }); @@ -387,6 +481,7 @@ function makePublicExecutionResult( from: AztecAddress, tx: FunctionCall, nestedExecutions: PublicExecutionResult[] = [], + contractStorageUpdateRequests: ContractStorageUpdateRequest[] = [], ): PublicExecutionResult { const callContext = new CallContext(from, tx.to, EthAddress.ZERO, tx.functionData.selector, false, false, false, 0); const execution: PublicExecution = { @@ -398,12 +493,12 @@ function makePublicExecutionResult( return { execution, nestedExecutions, + contractStorageUpdateRequests, returnValues: [], newNoteHashes: [], newNullifiers: [], newL2ToL1Messages: [], contractStorageReads: [], - contractStorageUpdateRequests: [], unencryptedLogs: new FunctionL2Logs([]), }; } diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index 9fd6c50b448..814570d016b 100644 --- a/yarn-project/simulator/src/public/executor.ts +++ b/yarn-project/simulator/src/public/executor.ts @@ -61,11 +61,17 @@ export async function executePublicFunction( const newNullifiers = newNullifiersPadded.filter(v => !v.isEmpty()); const { contractStorageReads, contractStorageUpdateRequests } = context.getStorageActionData(); + log( `Contract storage reads: ${contractStorageReads .map(r => r.toFriendlyJSON() + ` - sec: ${r.sideEffectCounter}`) .join(', ')}`, ); + log( + `Contract storage update requests: ${contractStorageUpdateRequests + .map(r => r.toFriendlyJSON() + ` - sec: ${r.sideEffectCounter}`) + .join(', ')}`, + ); const nestedExecutions = context.getNestedExecutions(); const unencryptedLogs = context.getUnencryptedLogs(); diff --git a/yarn-project/simulator/src/public/public_execution_context.ts b/yarn-project/simulator/src/public/public_execution_context.ts index 6f229971b5e..c60bfedacbd 100644 --- a/yarn-project/simulator/src/public/public_execution_context.ts +++ b/yarn-project/simulator/src/public/public_execution_context.ts @@ -138,7 +138,7 @@ export class PublicExecutionContext extends TypedOracle { public async storageWrite(startStorageSlot: Fr, values: Fr[]) { const newValues = []; for (let i = 0; i < values.length; i++) { - const storageSlot = new Fr(startStorageSlot.value + BigInt(i)); + const storageSlot = new Fr(startStorageSlot.toBigInt() + BigInt(i)); const newValue = values[i]; const sideEffectCounter = this.sideEffectCounter.count(); this.storageActions.write(storageSlot, newValue, sideEffectCounter); diff --git a/yarn-project/simulator/src/public/state_actions.ts b/yarn-project/simulator/src/public/state_actions.ts index b1e09ead6e2..dd995566e30 100644 --- a/yarn-project/simulator/src/public/state_actions.ts +++ b/yarn-project/simulator/src/public/state_actions.ts @@ -58,7 +58,7 @@ export class ContractStorageActionsCollector { * @param sideEffectCounter - Side effect counter associated with this storage action. */ public write(storageSlot: Fr, newValue: Fr, sideEffectCounter: number): void { - const slot = storageSlot.value; + const slot = storageSlot.toBigInt(); const updateRequest = this.contractStorageUpdateRequests.get(slot); if (updateRequest) { this.contractStorageUpdateRequests.set(slot, { newValue, sideEffectCounter });