diff --git a/noir-projects/noir-contracts/bootstrap.sh b/noir-projects/noir-contracts/bootstrap.sh index d843e9cdc7d..b5bf4764cb2 100755 --- a/noir-projects/noir-contracts/bootstrap.sh +++ b/noir-projects/noir-contracts/bootstrap.sh @@ -20,5 +20,4 @@ NARGO=${NARGO:-../../noir/noir-repo/target/release/nargo} $NARGO compile --silence-warnings echo "Transpiling avm contracts... (only '#[aztec(public-vm)]')" -TRANSPILER=${TRANSPILER:-../../avm-transpiler/target/release/avm-transpiler} -ls target/avm_*.json | parallel "$TRANSPILER {} {}" \ No newline at end of file +scripts/transpile.sh \ No newline at end of file diff --git a/noir-projects/noir-contracts/scripts/transpile.sh b/noir-projects/noir-contracts/scripts/transpile.sh new file mode 100755 index 00000000000..9bea61f5ffa --- /dev/null +++ b/noir-projects/noir-contracts/scripts/transpile.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -eu + +TRANSPILER=${TRANSPILER:-../../avm-transpiler/target/release/avm-transpiler} +ls target/avm_*.json | parallel "$TRANSPILER {} {}" \ No newline at end of file diff --git a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/kernel_circuit_public_inputs_composer.nr b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/kernel_circuit_public_inputs_composer.nr index 84157b9b95b..689393b9bbf 100644 --- a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/kernel_circuit_public_inputs_composer.nr +++ b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/kernel_circuit_public_inputs_composer.nr @@ -22,6 +22,9 @@ fn asc_sort_by_counters(a: T, b: T) -> bool where T: Ordered { a.counter() < b.counter() } +// Builds: +// .finish -> KernelCircuitPublicInputs (from PrivateKernelTailCircuitPrivateInputs) +// .finish_to_public -> PublicKernelCircuitPublicInputs (from PrivateKernelTailToPublicCircuitPrivateInputs) struct KernelCircuitPublicInputsComposer { public_inputs: PrivateKernelCircuitPublicInputsBuilder, previous_kernel: PrivateKernelData, @@ -47,7 +50,7 @@ impl KernelCircuitPublicInputsComposer { sorted_encrypted_log_hashes: [SideEffect; MAX_ENCRYPTED_LOGS_PER_TX], sorted_encrypted_log_hashes_indexes: [u64; MAX_ENCRYPTED_LOGS_PER_TX], sorted_unencrypted_log_hashes: [SideEffect; MAX_UNENCRYPTED_LOGS_PER_TX], - sorted_unencrypted_log_hashes_indexes: [u64; MAX_UNENCRYPTED_LOGS_PER_TX], + sorted_unencrypted_log_hashes_indexes: [u64; MAX_UNENCRYPTED_LOGS_PER_TX] ) -> Self { let public_inputs = PrivateKernelCircuitPublicInputsBuilder::empty(); @@ -62,7 +65,7 @@ impl KernelCircuitPublicInputsComposer { sorted_encrypted_log_hashes, sorted_encrypted_log_hashes_indexes, sorted_unencrypted_log_hashes, - sorted_unencrypted_log_hashes_indexes, + sorted_unencrypted_log_hashes_indexes } } @@ -82,6 +85,8 @@ impl KernelCircuitPublicInputsComposer { self.silo_values(); + self.set_gas_used(); + *self } @@ -102,6 +107,12 @@ impl KernelCircuitPublicInputsComposer { self.public_inputs.finish_to_public(min_revertible_side_effect_counter) } + fn set_gas_used(&mut self) { + // TODO(gas): Compute DA gas used here and add to public_inputs.(end,end_non_revertible).gas_used + let teardown_gas = self.previous_kernel.public_inputs.constants.tx_context.gas_settings.teardown_gas_limits; + self.public_inputs.end.gas_used = teardown_gas; + } + fn silo_values(&mut self) { self.silo_note_hashes(); // TODO: Move siloing from init/inner circuits to here. diff --git a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_init.nr b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_init.nr index 64bc2e703dd..b348adc4ad0 100644 --- a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_init.nr +++ b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_init.nr @@ -50,6 +50,10 @@ impl PrivateKernelInitCircuitPrivateInputs { // Ensure we are passing the correct arguments to the function. let args_match = tx_request.args_hash == call_stack_item.public_inputs.args_hash; assert(args_match, "noir function args passed to tx_request must match args in the call_stack_item"); + // + // Ensure we are passing the correct tx context + let tx_context_matches = tx_request.tx_context == call_stack_item.public_inputs.tx_context; + assert(tx_context_matches, "tx_context in tx_request must match tx_context in call_stack_item"); } fn validate_inputs(self) { diff --git a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_tail.nr b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_tail.nr index 5d360bb0024..730d2979dee 100644 --- a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_tail.nr +++ b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_tail.nr @@ -57,7 +57,7 @@ impl PrivateKernelTailCircuitPrivateInputs { self.sorted_encrypted_log_hashes, self.sorted_encrypted_log_hashes_indexes, self.sorted_unencrypted_log_hashes, - self.sorted_unencrypted_log_hashes_indexes, + self.sorted_unencrypted_log_hashes_indexes ); composer.compose().finish() } @@ -76,7 +76,7 @@ mod tests { use dep::types::{ abis::{ kernel_circuit_public_inputs::KernelCircuitPublicInputs, max_block_number::MaxBlockNumber, - side_effect::{SideEffect, SideEffectLinkedToNoteHash, Ordered} + side_effect::{SideEffect, SideEffectLinkedToNoteHash, Ordered}, gas::Gas }, grumpkin_private_key::GrumpkinPrivateKey, hash::{compute_note_hash_nonce, compute_unique_siloed_note_hash, accumulate_sha256}, @@ -189,7 +189,7 @@ mod tests { sorted_encrypted_log_hashes_indexes, sorted_unencrypted_log_hashes, sorted_unencrypted_log_hashes_indexes, - master_nullifier_secret_keys: [GrumpkinPrivateKey::empty(); MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX], + master_nullifier_secret_keys: [GrumpkinPrivateKey::empty(); MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX] }; kernel.native_private_kernel_circuit_tail() } @@ -488,4 +488,14 @@ mod tests { builder.previous_kernel.new_nullifiers = BoundedVec::new(); builder.failed(); } + + #[test] + unconstrained fn set_teardown_gas_as_gas_used() { + // TODO(gas): When we compute DA gas used, we'll have to include it here as well. + let mut builder = PrivateKernelTailInputsBuilder::new(); + builder.previous_kernel.tx_context.gas_settings.teardown_gas_limits = Gas::new(300, 300, 300); + let public_inputs = builder.execute(); + + assert_eq(public_inputs.end.gas_used, Gas::new(300, 300, 300)); + } } diff --git a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_tail_to_public.nr b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_tail_to_public.nr index 081219df963..bdef6ba568d 100644 --- a/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_tail_to_public.nr +++ b/noir-projects/noir-protocol-circuits/crates/private-kernel-lib/src/private_kernel_tail_to_public.nr @@ -57,7 +57,7 @@ impl PrivateKernelTailToPublicCircuitPrivateInputs { self.sorted_encrypted_log_hashes, self.sorted_encrypted_log_hashes_indexes, self.sorted_unencrypted_log_hashes, - self.sorted_unencrypted_log_hashes_indexes, + self.sorted_unencrypted_log_hashes_indexes ); composer.compose_public().finish_to_public() } @@ -75,7 +75,7 @@ mod tests { }; use dep::types::{ abis::{ - kernel_circuit_public_inputs::PublicKernelCircuitPublicInputs, + kernel_circuit_public_inputs::PublicKernelCircuitPublicInputs, gas::Gas, side_effect::{SideEffect, SideEffectLinkedToNoteHash, Ordered} }, grumpkin_private_key::GrumpkinPrivateKey, @@ -196,7 +196,7 @@ mod tests { sorted_encrypted_log_hashes_indexes, sorted_unencrypted_log_hashes, sorted_unencrypted_log_hashes_indexes, - master_nullifier_secret_keys: [GrumpkinPrivateKey::empty(); MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX], + master_nullifier_secret_keys: [GrumpkinPrivateKey::empty(); MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_TX] }; kernel.execute() } @@ -538,4 +538,14 @@ mod tests { assert(is_empty_array(public_inputs.end.new_note_hashes)); assert(is_empty_array(public_inputs.end.new_nullifiers)); } + + #[test] + unconstrained fn set_teardown_gas_as_gas_used() { + // TODO(gas): When we compute DA gas used, we'll have to include it here as well. + let mut builder = PrivateKernelTailToPublicInputsBuilder::new(); + builder.previous_kernel.tx_context.gas_settings.teardown_gas_limits = Gas::new(300, 300, 300); + let public_inputs = builder.execute(); + + assert_eq(public_inputs.end.gas_used, Gas::new(300, 300, 300)); + } } diff --git a/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/common.nr b/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/common.nr index 7cff5c51619..855da0a6ec4 100644 --- a/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/common.nr +++ b/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/common.nr @@ -229,6 +229,22 @@ pub fn update_non_revertible_gas_used(public_call: PublicCallData, circuit_outpu .sub(accum_end_gas_used); } +// Validates that the start gas injected into the app circuit matches the remaining gas +pub fn validate_start_gas(public_call: PublicCallData, previous_kernel: PublicKernelData) { + let public_call_start_gas = public_call.call_stack_item.public_inputs.start_gas_left; + let tx_gas_limits = previous_kernel.public_inputs.constants.tx_context.gas_settings.gas_limits; + let computed_start_gas = tx_gas_limits.sub(previous_kernel.public_inputs.end.gas_used).sub(previous_kernel.public_inputs.end_non_revertible.gas_used); + assert( + public_call_start_gas == computed_start_gas, "Start gas for public phase does not match transaction gas left" + ); +} + +// Validates the transaction fee injected into the app circuit is zero for non-teardown phases +pub fn validate_transaction_fee_is_zero(public_call: PublicCallData) { + let transaction_fee = public_call.call_stack_item.public_inputs.transaction_fee; + assert(transaction_fee == 0, "Transaction fee must be zero on setup and app phases"); +} + pub fn update_public_end_non_revertible_values( public_call: PublicCallData, circuit_outputs: &mut PublicKernelCircuitPublicInputsBuilder diff --git a/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_app_logic.nr b/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_app_logic.nr index f722f087618..e372e50f43d 100644 --- a/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_app_logic.nr +++ b/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_app_logic.nr @@ -33,6 +33,9 @@ impl PublicKernelAppLogicCircuitPrivateInputs { // validate the inputs unique to having a previous public kernel self.validate_inputs(); + common::validate_start_gas(self.public_call, self.previous_kernel); + common::validate_transaction_fee_is_zero(self.public_call); + common::update_validation_requests(self.public_call, &mut public_inputs); common::update_revertible_gas_used(self.public_call, &mut public_inputs); @@ -464,4 +467,22 @@ mod tests { assert_eq(output.end.gas_used, Gas::new(500, 500, 500)); assert_eq(output.end_non_revertible.gas_used, Gas::new(0, 0, 0)); } + + #[test(should_fail_with="Start gas for public phase does not match transaction gas left")] + fn validates_start_gas() { + let mut builder = PublicKernelAppLogicCircuitPrivateInputsBuilder::new(); + + builder.public_call.public_inputs.start_gas_left = Gas::new(200, 100, 100); + + builder.failed(); + } + + #[test(should_fail_with="Transaction fee must be zero on setup and app phases")] + fn validates_transaction_fee() { + let mut builder = PublicKernelAppLogicCircuitPrivateInputsBuilder::new(); + + builder.public_call.public_inputs.transaction_fee = 10; + + builder.failed(); + } } diff --git a/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_setup.nr b/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_setup.nr index 9b0f1764a6a..e918d78ad64 100644 --- a/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_setup.nr +++ b/noir-projects/noir-protocol-circuits/crates/public-kernel-lib/src/public_kernel_setup.nr @@ -39,6 +39,9 @@ impl PublicKernelSetupCircuitPrivateInputs { // validate the inputs unique to having a previous private kernel self.validate_inputs(); + common::validate_start_gas(self.public_call, self.previous_kernel); + common::validate_transaction_fee_is_zero(self.public_call); + common::update_non_revertible_gas_used(self.public_call, &mut public_inputs); // Pops the item from the call stack and validates it against the current execution. @@ -519,4 +522,22 @@ mod tests { assert_eq(output.end_non_revertible.gas_used, Gas::new(500, 500, 500)); assert_eq(output.end.gas_used, Gas::new(100, 100, 100)); } + + #[test(should_fail_with="Start gas for public phase does not match transaction gas left")] + fn validates_start_gas() { + let mut builder = PublicKernelSetupCircuitPrivateInputsBuilder::new(); + + builder.public_call.public_inputs.start_gas_left = Gas::new(200, 100, 100); + + builder.failed(); + } + + #[test(should_fail_with="Transaction fee must be zero on setup and app phases")] + fn validates_transaction_fee() { + let mut builder = PublicKernelSetupCircuitPrivateInputsBuilder::new(); + + builder.public_call.public_inputs.transaction_fee = 10; + + builder.failed(); + } } 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 67b89c28167..9b19bb71dcc 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 @@ -1,7 +1,7 @@ use crate::common; use dep::types::abis::{ kernel_circuit_public_inputs::{PublicKernelCircuitPublicInputs, PublicKernelCircuitPublicInputsBuilder}, - kernel_data::PublicKernelData, public_call_data::PublicCallData + kernel_data::PublicKernelData, public_call_data::PublicCallData, gas_fees::GasFees }; struct PublicKernelTeardownCircuitPrivateInputs { @@ -22,6 +22,45 @@ impl PublicKernelTeardownCircuitPrivateInputs { assert(needs_teardown == true, "Cannot run unnecessary teardown circuit"); } + // Validates that the start gas injected into the app circuit matches the teardown gas limits set by the user + fn validate_start_gas(self) { + let public_call_start_gas = self.public_call.call_stack_item.public_inputs.start_gas_left; + let teardown_gas_limit = self.previous_kernel.public_inputs.constants.tx_context.gas_settings.teardown_gas_limits; + assert( + public_call_start_gas == teardown_gas_limit, "Start gas for teardown phase does not match teardown gas allocation" + ); + } + + // Validates the transaction fee injected into the app circuit is properly computed from gas_used and block gas_fees + fn validate_transaction_fee(self) { + let transaction_fee = self.public_call.call_stack_item.public_inputs.transaction_fee; + // Note that teardown_gas is already included in end.gas_used as it was injected by the private kernel + let total_gas_used = self.previous_kernel.public_inputs.end.gas_used.add(self.previous_kernel.public_inputs.end_non_revertible.gas_used); + // TODO(palla/gas): Load gas fees from a PublicConstantData struct that's currently missing + let block_gas_fees = GasFees::default(); + let inclusion_fee = self.previous_kernel.public_inputs.constants.tx_context.gas_settings.inclusion_fee; + let computed_transaction_fee = total_gas_used.compute_fee(block_gas_fees) + inclusion_fee; + + // dep::types::debug_log::debug_log_format( + // "Validating tx fee: total_gas_used.da={0} total_gas_used.l1={1} total_gas_used.l2={2} block_fee_per_gas.da={3} block_fee_per_gas.l1={4} block_fee_per_gas.l2={5} inclusion_fee={6} computed={7} actual={8}", + // [ + // total_gas_used.da_gas as Field, + // total_gas_used.l1_gas as Field, + // total_gas_used.l2_gas as Field, + // block_gas_fees.fee_per_da_gas as Field, + // block_gas_fees.fee_per_l1_gas as Field, + // block_gas_fees.fee_per_l2_gas as Field, + // inclusion_fee, + // computed_transaction_fee, + // transaction_fee + // ] + // ); + + assert( + transaction_fee == computed_transaction_fee, "Transaction fee on teardown phase does not match expected value" + ); + } + fn public_kernel_teardown(self) -> PublicKernelCircuitPublicInputs { // construct the circuit outputs let mut public_inputs = PublicKernelCircuitPublicInputsBuilder::empty(); @@ -42,6 +81,9 @@ impl PublicKernelTeardownCircuitPrivateInputs { let call_request = public_inputs.end_non_revertible.public_call_stack.pop(); common::validate_call_against_request(self.public_call, call_request); + self.validate_start_gas(); + self.validate_transaction_fee(); + common::update_validation_requests(self.public_call, &mut public_inputs); common::update_public_end_non_revertible_values(self.public_call, &mut public_inputs); @@ -60,7 +102,7 @@ mod tests { }; use dep::types::{ abis::{ - call_request::CallRequest, function_selector::FunctionSelector, + call_request::CallRequest, function_selector::FunctionSelector, gas::Gas, kernel_circuit_public_inputs::PublicKernelCircuitPublicInputs, public_data_read::PublicDataRead, public_data_update_request::PublicDataUpdateRequest }, @@ -347,12 +389,16 @@ mod tests { let public_inputs = builder.execute(); - assert_eq(public_inputs.end_non_revertible.encrypted_log_preimages_length, prev_encrypted_log_preimages_length); + assert_eq( + public_inputs.end_non_revertible.encrypted_log_preimages_length, prev_encrypted_log_preimages_length + ); assert_eq( public_inputs.end_non_revertible.unencrypted_log_preimages_length, unencrypted_log_preimages_length + prev_unencrypted_log_preimages_length ); assert_eq(public_inputs.end_non_revertible.encrypted_logs_hashes[0].value, prev_encrypted_logs_hash); - assert_eq(public_inputs.end_non_revertible.unencrypted_logs_hashes[0].value, prev_unencrypted_logs_hash); + assert_eq( + public_inputs.end_non_revertible.unencrypted_logs_hashes[0].value, prev_unencrypted_logs_hash + ); assert_eq(public_inputs.end_non_revertible.unencrypted_logs_hashes[1].value, unencrypted_logs_hash); } @@ -363,4 +409,20 @@ mod tests { builder.failed(); } + + #[test(should_fail_with="Start gas for teardown phase does not match teardown gas allocation")] + fn validates_start_gas() { + let mut builder = PublicKernelTeardownCircuitPrivateInputsBuilder::new(); + builder.public_call.public_inputs.start_gas_left = Gas::new(10, 20, 30); + + builder.failed(); + } + + #[test(should_fail_with="Transaction fee on teardown phase does not match expected value")] + fn validates_transaction_fee() { + let mut builder = PublicKernelTeardownCircuitPrivateInputsBuilder::new(); + builder.public_call.public_inputs.transaction_fee = 1234; + + builder.failed(); + } } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/private_accumulated_data_builder.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/private_accumulated_data_builder.nr index bc735cc1558..70d79d5180d 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/private_accumulated_data_builder.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/accumulated_data/private_accumulated_data_builder.nr @@ -18,6 +18,10 @@ use crate::{ traits::Empty }; +// Builds via PrivateKernelCircuitPublicInputsBuilder: +// .finish: PrivateKernelCircuitPublicInputs.end +// .to_combined: KernelCircuitPublicInputs.end +// .split_to_public: PublicKernelCircuitPublicInputs.(end,end_non_revertible) struct PrivateAccumulatedDataBuilder { new_note_hashes: BoundedVec, new_nullifiers: BoundedVec, @@ -35,10 +39,14 @@ struct PrivateAccumulatedDataBuilder { public_call_stack: BoundedVec, gas_used: Gas, + non_revertible_gas_used: Gas, } impl PrivateAccumulatedDataBuilder { pub fn finish(self) -> PrivateAccumulatedData { + assert(self.gas_used.is_empty()); + assert(self.non_revertible_gas_used.is_empty()); + PrivateAccumulatedData { new_note_hashes: self.new_note_hashes.storage, new_nullifiers: self.new_nullifiers.storage, @@ -66,7 +74,7 @@ impl PrivateAccumulatedDataBuilder { encrypted_log_preimages_length: self.encrypted_log_preimages_length, unencrypted_log_preimages_length: self.unencrypted_log_preimages_length, public_data_update_requests: [PublicDataUpdateRequest::empty(); MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_TX], - gas_used: self.gas_used + gas_used: self.gas_used.add(self.non_revertible_gas_used) } } @@ -127,6 +135,8 @@ impl PrivateAccumulatedDataBuilder { revertible_builder.encrypted_log_preimages_length = self.encrypted_log_preimages_length; revertible_builder.unencrypted_log_preimages_length = self.unencrypted_log_preimages_length; + revertible_builder.gas_used = self.gas_used; + non_revertible_builder.gas_used = self.non_revertible_gas_used; (non_revertible_builder.finish(), revertible_builder.finish()) } } @@ -134,7 +144,7 @@ impl PrivateAccumulatedDataBuilder { mod tests { use crate::{ abis::{ - accumulated_data::private_accumulated_data_builder::PrivateAccumulatedDataBuilder, + accumulated_data::private_accumulated_data_builder::PrivateAccumulatedDataBuilder, gas::Gas, call_request::CallRequest, caller_context::CallerContext, public_data_update_request::PublicDataUpdateRequest, side_effect::{SideEffect, SideEffectLinkedToNoteHash} @@ -202,6 +212,9 @@ mod tests { builder.public_call_stack.extend_from_array(non_revertible_public_stack); builder.public_call_stack.extend_from_array(revertible_public_call_stack); + builder.gas_used = Gas::new(20,20,20); + builder.non_revertible_gas_used = Gas::new(10,10,10); + let (non_revertible, revertible) = builder.split_to_public(7); assert(array_eq(non_revertible.new_note_hashes, non_revertible_commitments)); @@ -211,6 +224,9 @@ mod tests { assert(array_eq(revertible.new_note_hashes, revertible_commitments)); assert(array_eq(revertible.new_nullifiers, revertible_nullifiers)); assert(array_eq(revertible.public_call_stack, revertible_public_call_stack)); + + assert_eq(revertible.gas_used, Gas::new(20, 20, 20)); + assert_eq(non_revertible.gas_used, Gas::new(10, 10, 10)); } } @@ -227,6 +243,7 @@ impl Empty for PrivateAccumulatedDataBuilder { private_call_stack: BoundedVec::new(), public_call_stack: BoundedVec::new(), gas_used: Gas::empty(), + non_revertible_gas_used: Gas::empty(), } } } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/gas.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/gas.nr index d4c6297b98b..7a738d0e27c 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/gas.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/gas.nr @@ -1,7 +1,7 @@ use crate::{ abis::function_selector::FunctionSelector, address::{EthAddress, AztecAddress}, constants::GAS_LENGTH, hash::pedersen_hash, traits::{Deserialize, Hash, Serialize, Empty}, - abis::side_effect::Ordered, utils::reader::Reader + abis::side_effect::Ordered, utils::reader::Reader, abis::gas_fees::GasFees }; struct Gas { @@ -15,7 +15,7 @@ impl Gas { Self { da_gas, l1_gas, l2_gas } } - fn add(self, other: Gas) -> Self { + pub fn add(self, other: Gas) -> Self { Gas::new( self.da_gas + other.da_gas, self.l1_gas + other.l1_gas, @@ -23,13 +23,23 @@ impl Gas { ) } - fn sub(self, other: Gas) -> Self { + pub fn sub(self, other: Gas) -> Self { Gas::new( self.da_gas - other.da_gas, self.l1_gas - other.l1_gas, self.l2_gas - other.l2_gas ) } + + pub fn compute_fee(self, fees: GasFees) -> Field { + (self.da_gas as Field) * fees.fee_per_da_gas + + (self.l1_gas as Field) * fees.fee_per_l1_gas + + (self.l2_gas as Field) * fees.fee_per_l2_gas + } + + pub fn is_empty(self) -> bool { + (self.da_gas == 0) & (self.l1_gas == 0) & (self.l2_gas == 0) + } } impl Serialize for Gas { diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/gas_fees.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/gas_fees.nr index 5e9fa42c556..09d75aae0c8 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/gas_fees.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/gas_fees.nr @@ -14,6 +14,10 @@ impl GasFees { pub fn new(fee_per_da_gas: Field, fee_per_l1_gas: Field, fee_per_l2_gas: Field) -> Self { Self { fee_per_da_gas, fee_per_l1_gas, fee_per_l2_gas } } + + pub fn default() -> Self { + GasFees::new(1, 1, 1) + } } impl Serialize for GasFees { diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/abis/kernel_circuit_public_inputs/private_kernel_circuit_public_inputs_builder.nr b/noir-projects/noir-protocol-circuits/crates/types/src/abis/kernel_circuit_public_inputs/private_kernel_circuit_public_inputs_builder.nr index b4fc5217621..301dce0667d 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/abis/kernel_circuit_public_inputs/private_kernel_circuit_public_inputs_builder.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/abis/kernel_circuit_public_inputs/private_kernel_circuit_public_inputs_builder.nr @@ -11,6 +11,10 @@ use crate::{ mocked::AggregationObject, partial_state_reference::PartialStateReference, traits::Empty }; +// Builds: +// .finish: PrivateKernelCircuitPublicInputs +// .finish_tail: KernelCircuitPublicInputs (from KernelCircuitPublicInputsComposer) +// .finish_to_public: PublicKernelCircuitPublicInputs (from KernelCircuitPublicInputsComposer) struct PrivateKernelCircuitPublicInputsBuilder { aggregation_object: AggregationObject, min_revertible_side_effect_counter: u32, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixture_builder.nr b/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixture_builder.nr index 8a402fae806..eb50167d527 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixture_builder.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/tests/fixture_builder.nr @@ -50,6 +50,7 @@ struct FixtureBuilder { private_call_stack: BoundedVec, public_call_stack: BoundedVec, gas_used: Gas, + non_revertible_gas_used: Gas, // Validation requests. max_block_number: MaxBlockNumber, @@ -109,7 +110,8 @@ impl FixtureBuilder { min_revertible_side_effect_counter: 0, counter: 0, start_state: PartialStateReference::empty(), - gas_used: Gas::empty() + gas_used: Gas::empty(), + non_revertible_gas_used: Gas::empty() } } @@ -128,7 +130,8 @@ impl FixtureBuilder { unencrypted_log_preimages_length: self.unencrypted_log_preimages_length, private_call_stack: self.private_call_stack, public_call_stack: self.public_call_stack, - gas_used: self.gas_used + gas_used: self.gas_used, + non_revertible_gas_used: self.non_revertible_gas_used }; public_inputs.finish() } @@ -451,6 +454,7 @@ impl Empty for FixtureBuilder { counter: 0, start_state: PartialStateReference::empty(), gas_used: Gas::empty(), + non_revertible_gas_used: Gas::empty(), } } } diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/tests/private_call_data_builder.nr b/noir-projects/noir-protocol-circuits/crates/types/src/tests/private_call_data_builder.nr index c9844fd07ab..7bcb80dca85 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/tests/private_call_data_builder.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/tests/private_call_data_builder.nr @@ -89,7 +89,7 @@ impl PrivateCallDataBuilder { } pub fn build_tx_context(self) -> TxContext { - TxContext { chain_id: 1, version: 0, gas_settings: self.gas_settings } + self.public_inputs.build_tx_context() } pub fn append_private_call_requests(&mut self, num_requests: u64, is_delegate_call: bool) { @@ -170,24 +170,21 @@ impl PrivateCallDataBuilder { self.public_inputs.unencrypted_log_preimages_length += preimages_length; } - pub fn get_call_stack_item_hash(self) -> Field { - let call_stack_item = PrivateCallStackItem { + fn build_call_stack_item(self) -> PrivateCallStackItem { + PrivateCallStackItem { contract_address: self.contract_address, function_data: self.function_data, public_inputs: self.public_inputs.finish() - }; - call_stack_item.hash() + } } - pub fn finish(self) -> PrivateCallData { - let call_stack_item = PrivateCallStackItem { - contract_address: self.contract_address, - function_data: self.function_data, - public_inputs: self.public_inputs.finish() - }; + pub fn get_call_stack_item_hash(self) -> Field { + self.build_call_stack_item().hash() + } + pub fn finish(self) -> PrivateCallData { PrivateCallData { - call_stack_item, + call_stack_item: self.build_call_stack_item(), private_call_stack: self.private_call_stack.storage, public_call_stack: self.public_call_stack.storage, proof: self.proof, diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/tests/private_circuit_public_inputs_builder.nr b/noir-projects/noir-protocol-circuits/crates/types/src/tests/private_circuit_public_inputs_builder.nr index f29d76fceab..f3de910442d 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/tests/private_circuit_public_inputs_builder.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/tests/private_circuit_public_inputs_builder.nr @@ -85,6 +85,10 @@ impl PrivateCircuitPublicInputsBuilder { public_inputs } + pub fn build_tx_context(self) -> TxContext { + TxContext::new(self.chain_id, self.version, self.gas_settings) + } + pub fn finish(self) -> PrivateCircuitPublicInputs { PrivateCircuitPublicInputs { call_context: self.call_context, @@ -107,7 +111,7 @@ impl PrivateCircuitPublicInputsBuilder { encrypted_log_preimages_length: self.encrypted_log_preimages_length, unencrypted_log_preimages_length: self.unencrypted_log_preimages_length, historical_header: self.historical_header, - tx_context: TxContext::new(self.chain_id, self.version, self.gas_settings) + tx_context: self.build_tx_context() } } } diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index d0bfaa8ff32..5a57d935a31 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -13,6 +13,7 @@ import { LogType, MerkleTreeId, NullifierMembershipWitness, + type ProcessOutput, type ProverClient, PublicDataWitness, type SequencerConfig, @@ -634,7 +635,7 @@ export class AztecNodeService implements AztecNode { * Simulates the public part of a transaction with the current state. * @param tx - The transaction to simulate. **/ - public async simulatePublicCalls(tx: Tx) { + public async simulatePublicCalls(tx: Tx): Promise { this.log.info(`Simulating tx ${tx.getTxHash()}`); const blockNumber = (await this.blockSource.getBlockNumber()) + 1; @@ -660,6 +661,7 @@ export class AztecNodeService implements AztecNode { new WASMSimulator(), ); const processor = await publicProcessorFactory.create(prevHeader, newGlobalVariables); + // REFACTOR: Consider merging ProcessReturnValues into ProcessedTx const [processedTxs, failedTxs, returns] = await processor.process([tx]); if (failedTxs.length) { this.log.warn(`Simulated tx ${tx.getTxHash()} fails: ${failedTxs[0].error}`); @@ -671,7 +673,15 @@ export class AztecNodeService implements AztecNode { throw reverted[0].revertReason; } this.log.info(`Simulated tx ${tx.getTxHash()} succeeds`); - return returns[0]; + const [processedTx] = processedTxs; + return { + constants: processedTx.data.constants, + encryptedLogs: processedTx.encryptedLogs, + unencryptedLogs: processedTx.unencryptedLogs, + end: processedTx.data.end, + revertReason: processedTx.revertReason, + publicReturnValues: returns[0], + }; } public setConfig(config: Partial): Promise { diff --git a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts index 06a8e67abc9..6d34219e5e6 100644 --- a/yarn-project/aztec.js/src/contract/contract_function_interaction.ts +++ b/yarn-project/aztec.js/src/contract/contract_function_interaction.ts @@ -105,7 +105,7 @@ export class ContractFunctionInteraction extends BaseContractInteraction { const txRequest = await this.create(); const simulatedTx = await this.pxe.simulateTx(txRequest, true); this.txRequest = undefined; - const flattened = simulatedTx.publicReturnValues; + const flattened = simulatedTx.publicOutput?.publicReturnValues; return flattened ? decodeReturnValues(this.functionDao, flattened) : []; } } diff --git a/yarn-project/circuit-types/src/interfaces/aztec-node.ts b/yarn-project/circuit-types/src/interfaces/aztec-node.ts index 03e9ddbff8a..95cc81d5bc2 100644 --- a/yarn-project/circuit-types/src/interfaces/aztec-node.ts +++ b/yarn-project/circuit-types/src/interfaces/aztec-node.ts @@ -21,7 +21,7 @@ import { } from '../logs/index.js'; import { type MerkleTreeId } from '../merkle_tree_id.js'; import { type SiblingPath } from '../sibling_path/index.js'; -import { type ProcessReturnValues, type Tx, type TxHash, type TxReceipt } from '../tx/index.js'; +import { type ProcessOutput, type Tx, type TxHash, type TxReceipt } from '../tx/index.js'; import { type TxEffect } from '../tx_effect.js'; import { type SequencerConfig } from './configs.js'; import { type L2BlockNumber } from './l2_block_number.js'; @@ -282,7 +282,7 @@ export interface AztecNode { * This currently just checks that the transaction execution succeeds. * @param tx - The transaction to simulate. **/ - simulatePublicCalls(tx: Tx): Promise; + simulatePublicCalls(tx: Tx): Promise; /** * Updates the configuration of this node. diff --git a/yarn-project/circuit-types/src/mocks.ts b/yarn-project/circuit-types/src/mocks.ts index 9cc201d2feb..3d5c1092bd2 100644 --- a/yarn-project/circuit-types/src/mocks.ts +++ b/yarn-project/circuit-types/src/mocks.ts @@ -12,7 +12,11 @@ import { computeContractClassId, getContractClassFromArtifact, } from '@aztec/circuits.js'; -import { makePublicCallRequest } from '@aztec/circuits.js/testing'; +import { + makeCombinedAccumulatedData, + makeCombinedConstantData, + makePublicCallRequest, +} from '@aztec/circuits.js/testing'; import { type ContractArtifact } from '@aztec/foundation/abi'; import { makeTuple } from '@aztec/foundation/array'; import { times } from '@aztec/foundation/collection'; @@ -23,7 +27,7 @@ import { type ContractInstanceWithAddress, SerializableContractInstance } from ' import { EncryptedL2Log } from './logs/encrypted_l2_log.js'; import { EncryptedFunctionL2Logs, EncryptedTxL2Logs, Note, UnencryptedTxL2Logs } from './logs/index.js'; import { ExtendedNote } from './notes/index.js'; -import { type ProcessReturnValues, SimulatedTx, Tx, TxHash } from './tx/index.js'; +import { type ProcessOutput, type ProcessReturnValues, SimulatedTx, Tx, TxHash } from './tx/index.js'; /** * Testing utility to create empty logs composed from a single empty log. @@ -114,7 +118,15 @@ export const mockTxForRollup = (seed = 1, { hasLogs = false }: { hasLogs?: boole export const mockSimulatedTx = (seed = 1, hasLogs = true) => { const tx = mockTx(seed, { hasLogs }); const dec: ProcessReturnValues = [new Fr(1n), new Fr(2n), new Fr(3n), new Fr(4n)]; - return new SimulatedTx(tx, dec, dec); + const output: ProcessOutput = { + constants: makeCombinedConstantData(), + encryptedLogs: tx.encryptedLogs, + unencryptedLogs: tx.unencryptedLogs, + end: makeCombinedAccumulatedData(), + revertReason: undefined, + publicReturnValues: dec, + }; + return new SimulatedTx(tx, dec, output); }; export const randomContractArtifact = (): ContractArtifact => ({ diff --git a/yarn-project/circuit-types/src/tx/simulated_tx.test.ts b/yarn-project/circuit-types/src/tx/simulated_tx.test.ts index 167c91259fa..8dd9ccc5c25 100644 --- a/yarn-project/circuit-types/src/tx/simulated_tx.test.ts +++ b/yarn-project/circuit-types/src/tx/simulated_tx.test.ts @@ -10,7 +10,7 @@ describe('simulated_tx', () => { it('convert undefined effects to and from json', () => { const simulatedTx = mockSimulatedTx(); simulatedTx.privateReturnValues = undefined; - simulatedTx.publicReturnValues = undefined; + simulatedTx.publicOutput = undefined; expect(SimulatedTx.fromJSON(simulatedTx.toJSON())).toEqual(simulatedTx); }); }); diff --git a/yarn-project/circuit-types/src/tx/simulated_tx.ts b/yarn-project/circuit-types/src/tx/simulated_tx.ts index baef7a2d4ea..bf012e0f901 100644 --- a/yarn-project/circuit-types/src/tx/simulated_tx.ts +++ b/yarn-project/circuit-types/src/tx/simulated_tx.ts @@ -1,32 +1,59 @@ -import { Fr } from '@aztec/circuits.js'; +import { CombinedAccumulatedData, CombinedConstantData, Fr } from '@aztec/circuits.js'; +import { EncryptedTxL2Logs, UnencryptedTxL2Logs } from '../logs/index.js'; +import { type ProcessedTx } from './processed_tx.js'; import { Tx } from './tx.js'; +/** Return values of simulating a circuit. */ export type ProcessReturnValues = Fr[] | undefined; +/** + * Outputs of processing the public component of a transaction. + * REFACTOR: Rename. + */ +export type ProcessOutput = Pick & + Pick & { publicReturnValues: ProcessReturnValues }; + +function processOutputToJSON(output: ProcessOutput) { + return { + encryptedLogs: output.encryptedLogs.toJSON(), + unencryptedLogs: output.unencryptedLogs.toJSON(), + revertReason: output.revertReason, + constants: output.constants.toBuffer().toString('hex'), + end: output.end.toBuffer().toString('hex'), + publicReturnValues: output.publicReturnValues?.map(fr => fr.toString()), + }; +} + +function processOutputFromJSON(json: any): ProcessOutput { + return { + encryptedLogs: EncryptedTxL2Logs.fromJSON(json.encryptedLogs), + unencryptedLogs: UnencryptedTxL2Logs.fromJSON(json.unencryptedLogs), + revertReason: json.revertReason, + constants: CombinedConstantData.fromBuffer(Buffer.from(json.constants, 'hex')), + end: CombinedAccumulatedData.fromBuffer(Buffer.from(json.end, 'hex')), + publicReturnValues: json.publicReturnValues?.map(Fr.fromString), + }; +} + +// REFACTOR: Review what we need to expose to the user when running a simulation. +// Eg tx already has encrypted and unencrypted logs, but those cover only the ones +// emitted during private. We need the ones from ProcessOutput to include the public +// ones as well. However, those would only be present if the user chooses to simulate +// the public side of things. This also points at this class needing to be split into +// two: one with just private simulation, and one that also includes public simulation. export class SimulatedTx { - constructor( - public tx: Tx, - public privateReturnValues?: ProcessReturnValues, - public publicReturnValues?: ProcessReturnValues, - ) {} + constructor(public tx: Tx, public privateReturnValues?: ProcessReturnValues, public publicOutput?: ProcessOutput) {} /** * Convert a SimulatedTx class object to a plain JSON object. * @returns A plain object with SimulatedTx properties. */ public toJSON() { - const returnToJson = (data: ProcessReturnValues | undefined): string => { - if (data === undefined) { - return JSON.stringify(data); - } - return JSON.stringify(data.map(fr => fr.toString())); - }; - return { tx: this.tx.toJSON(), - privateReturnValues: returnToJson(this.privateReturnValues), - publicReturnValues: returnToJson(this.publicReturnValues), + privateReturnValues: this.privateReturnValues?.map(fr => fr.toString()), + publicOutput: this.publicOutput && processOutputToJSON(this.publicOutput), }; } @@ -36,17 +63,10 @@ export class SimulatedTx { * @returns A Tx class object. */ public static fromJSON(obj: any) { - const returnFromJson = (json: string): ProcessReturnValues | undefined => { - if (json === undefined) { - return json; - } - return JSON.parse(json).map(Fr.fromString); - }; - const tx = Tx.fromJSON(obj.tx); - const privateReturnValues = returnFromJson(obj.privateReturnValues); - const publicReturnValues = returnFromJson(obj.publicReturnValues); + const publicOutput = obj.publicOutput ? processOutputFromJSON(obj.publicOutput) : undefined; + const privateReturnValues = obj.privateReturnValues?.map(Fr.fromString); - return new SimulatedTx(tx, privateReturnValues, publicReturnValues); + return new SimulatedTx(tx, privateReturnValues, publicOutput); } } diff --git a/yarn-project/circuits.js/src/structs/gas.ts b/yarn-project/circuits.js/src/structs/gas.ts index 8380f362e25..b1e2a844c88 100644 --- a/yarn-project/circuits.js/src/structs/gas.ts +++ b/yarn-project/circuits.js/src/structs/gas.ts @@ -1,9 +1,10 @@ -import { type Fr } from '@aztec/foundation/fields'; +import { Fr } from '@aztec/foundation/fields'; import { BufferReader, FieldReader, serializeToBuffer, serializeToFields } from '@aztec/foundation/serialize'; import { type FieldsOf } from '@aztec/foundation/types'; import { inspect } from 'util'; +import { type GasFees } from './gas_fees.js'; import { type UInt32 } from './shared.js'; export const GasDimensions = ['da', 'l1', 'l2'] as const; @@ -25,8 +26,8 @@ export class Gas { return this.daGas === other.daGas && this.l1Gas === other.l1Gas && this.l2Gas === other.l2Gas; } - static from(fields: FieldsOf) { - return new Gas(fields.daGas, fields.l1Gas, fields.l2Gas); + static from(fields: Partial>) { + return new Gas(fields.daGas ?? 0, fields.l1Gas ?? 0, fields.l2Gas ?? 0); } static empty() { @@ -67,6 +68,13 @@ export class Gas { return new Gas(Math.ceil(this.daGas * scalar), Math.ceil(this.l1Gas * scalar), Math.ceil(this.l2Gas * scalar)); } + computeFee(gasFees: GasFees) { + return GasDimensions.reduce( + (acc, dimension) => acc.add(gasFees.get(dimension).mul(new Fr(this.get(dimension)))), + Fr.ZERO, + ); + } + toFields() { return serializeToFields(this.daGas, this.l1Gas, this.l2Gas); } diff --git a/yarn-project/circuits.js/src/tests/factories.ts b/yarn-project/circuits.js/src/tests/factories.ts index c6a1a81b2ef..52a9b1e1d0a 100644 --- a/yarn-project/circuits.js/src/tests/factories.ts +++ b/yarn-project/circuits.js/src/tests/factories.ts @@ -169,7 +169,7 @@ export function makeNewSideEffectLinkedToNoteHash(seed: number): SideEffectLinke * @param seed - The seed to use for generating the tx context. * @returns A tx context. */ -export function makeTxContext(seed: number): TxContext { +export function makeTxContext(seed: number = 1): TxContext { // @todo @LHerskind should probably take value for chainId as it will be verified later. return new TxContext(new Fr(seed), Fr.ZERO, makeGasSettings()); } @@ -292,6 +292,10 @@ export function makeRollupValidationRequests(seed = 1) { return new RollupValidationRequests(new MaxBlockNumber(true, new Fr(seed + 0x31415))); } +export function makeCombinedConstantData(seed = 1): CombinedConstantData { + return new CombinedConstantData(makeHeader(seed), makeTxContext(seed + 0x100)); +} + /** * Creates arbitrary accumulated data. * @param seed - The seed to use for generating the accumulated data. diff --git a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts index ac36f13ceaf..c4d153b3bed 100644 --- a/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts +++ b/yarn-project/end-to-end/src/e2e_avm_simulator.test.ts @@ -1,4 +1,5 @@ import { type AccountWallet, AztecAddress, Fr, FunctionSelector, TxStatus } from '@aztec/aztec.js'; +import { GasSettings } from '@aztec/circuits.js'; import { AvmAcvmInteropTestContract, AvmInitializerTestContract, @@ -32,6 +33,21 @@ describe('e2e_avm_simulator', () => { avmContract = await AvmTestContract.deploy(wallet).send().deployed(); }, 50_000); + describe('Gas metering', () => { + it('Tracks L2 gas usage on simulation', async () => { + const request = await avmContract.methods.add_args_return(20n, 30n).create(); + const simulation = await wallet.simulateTx(request, true, wallet.getAddress()); + // Subtract the teardown gas allocation from the gas used to figure out the gas used by the contract logic. + const l2TeardownAllocation = GasSettings.simulation().getTeardownLimits().l2Gas; + const l2GasUsed = simulation.publicOutput!.end.gasUsed.l2Gas! - l2TeardownAllocation; + // L2 gas used will vary a lot depending on codegen and other factors, + // so we just set a wide range for it, and check it's not a suspiciously round number. + expect(l2GasUsed).toBeGreaterThan(1e3); + expect(l2GasUsed).toBeLessThan(1e6); + expect(l2GasUsed! % 1000).not.toEqual(0); + }); + }); + describe('Storage', () => { it('Modifies storage (Field)', async () => { await avmContract.methods.set_storage_single(20n).send().wait(); diff --git a/yarn-project/prover-client/src/mocks/test_context.ts b/yarn-project/prover-client/src/mocks/test_context.ts index 42ecc4985c0..0fe75860cc0 100644 --- a/yarn-project/prover-client/src/mocks/test_context.ts +++ b/yarn-project/prover-client/src/mocks/test_context.ts @@ -1,5 +1,6 @@ import { type BlockProver, type ProcessedTx, type Tx, type TxValidator } from '@aztec/circuit-types'; -import { GlobalVariables, Header } from '@aztec/circuits.js'; +import { type Gas, GlobalVariables, Header, type TxContext } from '@aztec/circuits.js'; +import { type Fr } from '@aztec/foundation/fields'; import { type DebugLogger } from '@aztec/foundation/log'; import { openTmpStore } from '@aztec/kv-store/utils'; import { @@ -121,11 +122,22 @@ export class TestContext { blockProver?: BlockProver, txValidator?: TxValidator, ) { - const defaultExecutorImplementation = (execution: PublicExecution, _1: GlobalVariables, _2?: number) => { + const defaultExecutorImplementation = ( + execution: PublicExecution, + _globalVariables: GlobalVariables, + availableGas: Gas, + _txContext: TxContext, + transactionFee?: Fr, + _sideEffectCounter?: number, + ) => { for (const tx of txs) { for (const request of tx.enqueuedPublicFunctionCalls) { if (execution.contractAddress.equals(request.contractAddress)) { - const result = PublicExecutionResultBuilder.fromPublicCallRequest({ request }).build(); + const result = PublicExecutionResultBuilder.fromPublicCallRequest({ request }).build({ + startGasLeft: availableGas, + endGasLeft: availableGas, + transactionFee, + }); // result.unencryptedLogs = tx.unencryptedLogs.functionLogs[0]; return Promise.resolve(result); } @@ -150,6 +162,9 @@ export class TestContext { executorMock?: ( execution: PublicExecution, globalVariables: GlobalVariables, + availableGas: Gas, + txContext: TxContext, + transactionFee?: Fr, sideEffectCounter?: number, ) => Promise, ) { diff --git a/yarn-project/pxe/src/pxe_service/pxe_service.ts b/yarn-project/pxe/src/pxe_service/pxe_service.ts index c3d5de0c33d..e87a447f81b 100644 --- a/yarn-project/pxe/src/pxe_service/pxe_service.ts +++ b/yarn-project/pxe/src/pxe_service/pxe_service.ts @@ -420,7 +420,7 @@ export class PXEService implements PXE { txRequest: TxExecutionRequest, simulatePublic: boolean, msgSender: AztecAddress | undefined = undefined, - ) { + ): Promise { if (!txRequest.functionData.isPrivate) { throw new Error(`Public entrypoints are not allowed`); } @@ -441,7 +441,7 @@ export class PXEService implements PXE { } if (simulatePublic) { - simulatedTx.publicReturnValues = await this.#simulatePublicCalls(simulatedTx.tx); + simulatedTx.publicOutput = await this.#simulatePublicCalls(simulatedTx.tx); } if (!msgSender) { diff --git a/yarn-project/simulator/src/mocks/fixtures.ts b/yarn-project/simulator/src/mocks/fixtures.ts index 2930fd627d0..f7fd2d72562 100644 --- a/yarn-project/simulator/src/mocks/fixtures.ts +++ b/yarn-project/simulator/src/mocks/fixtures.ts @@ -102,7 +102,7 @@ export class PublicExecutionResultBuilder { return this; } - build(): PublicExecutionResult { + build(overrides: Partial = {}): PublicExecutionResult { return { execution: this._execution, nestedExecutions: this._nestedExecutions, @@ -120,7 +120,10 @@ export class PublicExecutionResultBuilder { endSideEffectCounter: Fr.ZERO, reverted: this._reverted, revertReason: this._revertReason, - gasLeft: Gas.test(), // TODO(palla/gas): Set a proper value + startGasLeft: Gas.test(), + endGasLeft: Gas.test(), + transactionFee: Fr.ZERO, + ...overrides, }; } } diff --git a/yarn-project/simulator/src/public/abstract_phase_manager.ts b/yarn-project/simulator/src/public/abstract_phase_manager.ts index 4564b7d4130..27f513dd4d2 100644 --- a/yarn-project/simulator/src/public/abstract_phase_manager.ts +++ b/yarn-project/simulator/src/public/abstract_phase_manager.ts @@ -227,6 +227,9 @@ export abstract class AbstractPhaseManager { const newUnencryptedFunctionLogs: UnencryptedFunctionL2Logs[] = []; + // Transaction fee is zero for all phases except teardown + const transactionFee = this.getTransactionFee(tx, previousPublicKernelOutput); + // TODO(#1684): Should multiple separately enqueued public calls be treated as // separate public callstacks to be proven by separate public kernel sequences // and submitted separately to the base rollup? @@ -243,9 +246,17 @@ export abstract class AbstractPhaseManager { const current = executionStack.pop()!; const isExecutionRequest = !isPublicExecutionResult(current); const sideEffectCounter = lastSideEffectCounter(tx) + 1; + const availableGas = this.getAvailableGas(tx, previousPublicKernelOutput); const result = isExecutionRequest - ? await this.publicExecutor.simulate(current, this.globalVariables, sideEffectCounter) + ? await this.publicExecutor.simulate( + current, + this.globalVariables, + availableGas, + tx.data.constants.txContext, + transactionFee, + sideEffectCounter, + ) : current; const functionSelector = result.execution.functionData.selector.toString(); @@ -308,6 +319,17 @@ export abstract class AbstractPhaseManager { return [publicKernelInputs, kernelOutput, kernelProof, newUnencryptedFunctionLogs, undefined, returns]; } + protected getAvailableGas(tx: Tx, previousPublicKernelOutput: PublicKernelCircuitPublicInputs) { + return tx.data.constants.txContext.gasSettings + .getLimits() // No need to subtract teardown limits since they are already included in end.gasUsed + .sub(previousPublicKernelOutput.end.gasUsed) + .sub(previousPublicKernelOutput.endNonRevertibleData.gasUsed); + } + + protected getTransactionFee(_tx: Tx, _previousPublicKernelOutput: PublicKernelCircuitPublicInputs) { + return Fr.ZERO; + } + protected async runKernelCircuit( previousOutput: PublicKernelCircuitPublicInputs, previousProof: Proof, @@ -348,7 +370,7 @@ export abstract class AbstractPhaseManager { return new PublicKernelData(previousOutput, previousProof, vk, vkIndex, vkSiblingPath); } - protected async getPublicCircuitPublicInputs(result: PublicExecutionResult) { + protected async getPublicCallStackItem(result: PublicExecutionResult, isExecutionRequest = false) { const publicDataTreeInfo = await this.db.getTreeInfo(MerkleTreeId.PUBLIC_DATA_TREE); this.historicalHeader.state.partial.publicDataTree.root = Fr.fromBuffer(publicDataTreeInfo.root); @@ -361,7 +383,7 @@ export abstract class AbstractPhaseManager { const unencryptedLogPreimagesLength = new Fr(result.unencryptedLogs.getSerializedLength()); - return PublicCircuitPublicInputs.from({ + const publicCircuitPublicInputs = PublicCircuitPublicInputs.from({ callContext: result.execution.callContext, proverAddress: AztecAddress.ZERO, argsHash: computeVarArgsHash(result.execution.args), @@ -399,20 +421,17 @@ export abstract class AbstractPhaseManager { ), unencryptedLogPreimagesLength, historicalHeader: this.historicalHeader, + startGasLeft: Gas.from(result.startGasLeft), + endGasLeft: Gas.from(result.endGasLeft), + transactionFee: result.transactionFee, // TODO(@just-mitch): need better mapping from simulator to revert code. revertCode: result.reverted ? RevertCode.REVERTED : RevertCode.OK, - // TODO(palla/gas): Set proper values - startGasLeft: Gas.test(), - endGasLeft: Gas.test(), - transactionFee: Fr.ZERO, }); - } - protected async getPublicCallStackItem(result: PublicExecutionResult, isExecutionRequest = false) { return new PublicCallStackItem( result.execution.contractAddress, result.execution.functionData, - await this.getPublicCircuitPublicInputs(result), + publicCircuitPublicInputs, isExecutionRequest, ); } diff --git a/yarn-project/simulator/src/public/execution.ts b/yarn-project/simulator/src/public/execution.ts index 871002e9635..25523f24a67 100644 --- a/yarn-project/simulator/src/public/execution.ts +++ b/yarn-project/simulator/src/public/execution.ts @@ -61,8 +61,12 @@ export interface PublicExecutionResult { * The revert reason if the execution reverted. */ revertReason: SimulationError | undefined; + /** How much gas was available for this public execution. */ + startGasLeft: Gas; /** How much gas was left after this public execution. */ - gasLeft: Gas; // TODO(palla/gas): Check this field + endGasLeft: Gas; + /** Transaction fee set for this tx. */ + transactionFee: Fr; } /** diff --git a/yarn-project/simulator/src/public/executor.ts b/yarn-project/simulator/src/public/executor.ts index 57710e7c5a8..9e5c3a098c1 100644 --- a/yarn-project/simulator/src/public/executor.ts +++ b/yarn-project/simulator/src/public/executor.ts @@ -1,5 +1,12 @@ import { UnencryptedFunctionL2Logs } from '@aztec/circuit-types'; -import { Fr, Gas, type GlobalVariables, type Header, PublicCircuitPublicInputs } from '@aztec/circuits.js'; +import { + Fr, + Gas, + type GlobalVariables, + type Header, + PublicCircuitPublicInputs, + type TxContext, +} from '@aztec/circuits.js'; import { createDebugLogger } from '@aztec/foundation/log'; import { spawn } from 'child_process'; @@ -64,9 +71,11 @@ async function executePublicFunctionAvm(executionContext: PublicExecutionContext executionContext.execution, executionContext.header, executionContext.globalVariables, + executionContext.gasSettings, + executionContext.transactionFee, ); - const machineState = new AvmMachineState(Gas.test()); // TODO(palla/gas): Set proper values + const machineState = new AvmMachineState(executionContext.availableGas); const context = new AvmContext(worldStateJournal, executionEnv, machineState); const simulator = new AvmSimulator(context); @@ -152,7 +161,9 @@ async function executePublicFunctionAcvm( unencryptedLogs: UnencryptedFunctionL2Logs.empty(), reverted, revertReason, - gasLeft: Gas.empty(), + startGasLeft: context.availableGas, + endGasLeft: Gas.empty(), + transactionFee: context.transactionFee, }; } @@ -196,7 +207,8 @@ async function executePublicFunctionAcvm( const nestedExecutions = context.getNestedExecutions(); const unencryptedLogs = context.getUnencryptedLogs(); - const gasLeft = Gas.test(); // TODO(palla/gas): Set proper value + const startGasLeft = context.availableGas; + const endGasLeft = context.availableGas; // No gas consumption in non-AVM return { execution, @@ -215,7 +227,9 @@ async function executePublicFunctionAcvm( unencryptedLogs, reverted: false, revertReason: undefined, - gasLeft, + startGasLeft, + endGasLeft, + transactionFee: context.transactionFee, }; } @@ -240,6 +254,9 @@ export class PublicExecutor { public async simulate( execution: PublicExecution, globalVariables: GlobalVariables, + availableGas: Gas, + txContext: TxContext, + transactionFee: Fr = Fr.ZERO, sideEffectCounter: number = 0, ): Promise { // Functions can request to pack arguments before calling other functions. @@ -255,6 +272,9 @@ export class PublicExecutor { this.stateDb, this.contractsDb, this.commitmentsDb, + availableGas, + transactionFee, + txContext.gasSettings, ); const executionResult = await executePublicFunction(context, /*nested=*/ false); diff --git a/yarn-project/simulator/src/public/index.test.ts b/yarn-project/simulator/src/public/index.test.ts index 8e8fb651afe..644b1e970ea 100644 --- a/yarn-project/simulator/src/public/index.test.ts +++ b/yarn-project/simulator/src/public/index.test.ts @@ -3,6 +3,7 @@ import { AppendOnlyTreeSnapshot, CallContext, FunctionData, + Gas, GasFees, GlobalVariables, type Header, @@ -13,7 +14,7 @@ import { NullifierLeafPreimage, } from '@aztec/circuits.js'; import { computeInnerNoteHash, computeNoteContentHash, siloNullifier } from '@aztec/circuits.js/hash'; -import { makeHeader } from '@aztec/circuits.js/testing'; +import { makeHeader, makeTxContext } from '@aztec/circuits.js/testing'; import { type FunctionArtifact, FunctionSelector, encodeArguments } from '@aztec/foundation/abi'; import { AztecAddress } from '@aztec/foundation/aztec-address'; import { pedersenHash, randomInt } from '@aztec/foundation/crypto'; @@ -97,6 +98,9 @@ describe('ACIR public execution simulator', () => { ...overrides, }); + const simulate = (execution: PublicExecution, globalVariables: GlobalVariables) => + executor.simulate(execution, globalVariables, Gas.test(), makeTxContext(), Fr.ZERO); + describe('Token contract', () => { let recipient: AztecAddress; let contractAddress: AztecAddress; @@ -130,7 +134,7 @@ describe('ACIR public execution simulator', () => { .mockResolvedValueOnce(previousTotalSupply); // reading total supply const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - const result = await executor.simulate(execution, globalVariables); + const result = await simulate(execution, globalVariables); expect(result.revertReason).toBeUndefined(); const recipientBalanceStorageSlot = computeSlotForMapping(new Fr(6n), recipient); @@ -212,7 +216,7 @@ describe('ACIR public execution simulator', () => { const recipientBalance = new Fr(20n); mockStore(senderBalance, recipientBalance); - const result = await executor.simulate(execution, globalVariables); + const result = await simulate(execution, globalVariables); const expectedRecipientBalance = new Fr(160n); const expectedSenderBalance = new Fr(60n); @@ -240,7 +244,7 @@ describe('ACIR public execution simulator', () => { const recipientBalance = new Fr(20n); mockStore(senderBalance, recipientBalance); - const { reverted, revertReason } = await executor.simulate(execution, globalVariables); + const { reverted, revertReason } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch('Assertion failed: attempt to subtract with underflow'); }); @@ -289,7 +293,7 @@ describe('ACIR public execution simulator', () => { new GasFees(new Fr(10), new Fr(11), new Fr(12)), ); - const result = await executor.simulate(execution, globalVariables); + const result = await simulate(execution, globalVariables); expect(result.returnValues[0]).toEqual( new Fr( initialValue + @@ -329,7 +333,7 @@ describe('ACIR public execution simulator', () => { publicState.storageRead.mockResolvedValue(amount); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - const result = await executor.simulate(execution, globalVariables); + const result = await simulate(execution, globalVariables); // Assert the note hash was created expect(result.newNoteHashes.length).toEqual(1); @@ -352,7 +356,7 @@ describe('ACIR public execution simulator', () => { publicContracts.getBytecode.mockResolvedValue(createL2ToL1MessagePublicArtifact.bytecode); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - const result = await executor.simulate(execution, globalVariables); + const result = await simulate(execution, globalVariables); // Assert the l2 to l1 message was created expect(result.newL2ToL1Messages.length).toEqual(1); @@ -374,7 +378,7 @@ describe('ACIR public execution simulator', () => { publicContracts.getBytecode.mockResolvedValue(createNullifierPublicArtifact.bytecode); const execution: PublicExecution = { contractAddress, functionData, args, callContext }; - const result = await executor.simulate(execution, globalVariables); + const result = await simulate(execution, globalVariables); // Assert the l2 to l1 message was created expect(result.newNullifiers.length).toEqual(1); @@ -476,7 +480,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const result = await executor.simulate(execution, globalVariables); + const result = await simulate(execution, globalVariables); expect(result.newNullifiers.length).toEqual(1); }); @@ -493,7 +497,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const { revertReason, reverted } = await executor.simulate(execution, globalVariables); + const { revertReason, reverted } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch(`Message not in state`); }); @@ -510,7 +514,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const { revertReason, reverted } = await executor.simulate(execution, globalVariables); + const { revertReason, reverted } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch(`Message not in state`); }); @@ -527,7 +531,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const { revertReason, reverted } = await executor.simulate(execution, globalVariables); + const { revertReason, reverted } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch(`Message not in state`); }); @@ -544,7 +548,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const { revertReason, reverted } = await executor.simulate(execution, globalVariables); + const { revertReason, reverted } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch(`Message not in state`); }); @@ -561,7 +565,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const { revertReason, reverted } = await executor.simulate(execution, globalVariables); + const { revertReason, reverted } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch(`Message not in state`); }); @@ -579,7 +583,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const { revertReason, reverted } = await executor.simulate(execution, globalVariables); + const { revertReason, reverted } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch(`Message not in state`); }); @@ -597,7 +601,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const { revertReason, reverted } = await executor.simulate(execution, globalVariables); + const { revertReason, reverted } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch(`Message not in state`); }); @@ -664,7 +668,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - expect(() => executor.simulate(execution, globalVariables)).not.toThrow(); + expect(() => simulate(execution, globalVariables)).not.toThrow(); }); it('Invalid', async () => { @@ -680,7 +684,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const { revertReason, reverted } = await executor.simulate(execution, globalVariables); + const { revertReason, reverted } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch(`Invalid ${description.toLowerCase()}`); }); @@ -711,7 +715,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - expect(() => executor.simulate(execution, globalVariables)).not.toThrow(); + expect(() => simulate(execution, globalVariables)).not.toThrow(); }); it('Throws when header is not as expected', async () => { @@ -721,7 +725,7 @@ describe('ACIR public execution simulator', () => { const execution: PublicExecution = { contractAddress, functionData, args, callContext }; executor = new PublicExecutor(publicState, publicContracts, commitmentsDb, header); - const { revertReason, reverted } = await executor.simulate(execution, globalVariables); + const { revertReason, reverted } = await simulate(execution, globalVariables); expect(reverted).toBe(true); expect(revertReason?.message).toMatch(`Invalid header hash`); }); diff --git a/yarn-project/simulator/src/public/public_execution_context.ts b/yarn-project/simulator/src/public/public_execution_context.ts index c3ac433ce29..7f16fd7a19b 100644 --- a/yarn-project/simulator/src/public/public_execution_context.ts +++ b/yarn-project/simulator/src/public/public_execution_context.ts @@ -3,7 +3,8 @@ import { CallContext, FunctionData, type FunctionSelector, - Gas, + type Gas, + type GasSettings, type GlobalVariables, type Header, PublicContextInputs, @@ -40,6 +41,9 @@ export class PublicExecutionContext extends TypedOracle { public readonly stateDb: PublicStateDB, public readonly contractsDb: PublicContractsDB, public readonly commitmentsDb: CommitmentsDB, + public readonly availableGas: Gas, + public readonly transactionFee: Fr, + public readonly gasSettings: GasSettings, private log = createDebugLogger('aztec:simulator:public_execution_context'), ) { super(); @@ -62,8 +66,8 @@ export class PublicExecutionContext extends TypedOracle { this.header, this.globalVariables, this.sideEffectCounter.current(), - Gas.test(), // TODO(palla/gas): Set proper values - new Fr(0), + this.availableGas, + this.transactionFee, ); const fields = [...publicContextInputs.toFields(), ...args]; return toACVMWitness(witnessStartIndex, fields); @@ -222,6 +226,9 @@ export class PublicExecutionContext extends TypedOracle { this.stateDb, this.contractsDb, this.commitmentsDb, + this.availableGas, + this.transactionFee, + this.gasSettings, this.log, ); diff --git a/yarn-project/simulator/src/public/public_processor.test.ts b/yarn-project/simulator/src/public/public_processor.test.ts index 904321e2d6c..7f52b6352f9 100644 --- a/yarn-project/simulator/src/public/public_processor.test.ts +++ b/yarn-project/simulator/src/public/public_processor.test.ts @@ -12,6 +12,9 @@ import { AppendOnlyTreeSnapshot, ContractStorageUpdateRequest, Fr, + Gas, + GasFees, + GasSettings, GlobalVariables, Header, PUBLIC_DATA_TREE_HEIGHT, @@ -25,6 +28,7 @@ import { import { computePublicDataTreeLeafSlot } from '@aztec/circuits.js/hash'; import { fr, makeAztecAddress, makePublicCallRequest, makeSelector } from '@aztec/circuits.js/testing'; import { arrayNonEmptyLength } from '@aztec/foundation/collection'; +import { type FieldsOf } from '@aztec/foundation/types'; import { openTmpStore } from '@aztec/kv-store/utils'; import { type AppendOnlyTree, Pedersen, StandardTree, newTree } from '@aztec/merkle-tree'; import { type PublicExecutionResult, type PublicExecutor, WASMSimulator } from '@aztec/simulator'; @@ -197,7 +201,7 @@ describe('public_processor', () => { db, publicExecutor, publicKernel, - GlobalVariables.empty(), + GlobalVariables.from({ ...GlobalVariables.empty(), gasFees: GasFees.default() }), header, publicContractsDB, publicWorldStateDB, @@ -345,6 +349,9 @@ describe('public_processor', () => { publicCallRequests, }); + const teardownGas = tx.data.constants.txContext.gasSettings.getTeardownLimits(); + const teardownResultSettings = { startGasLeft: teardownGas, endGasLeft: teardownGas }; + const contractSlotA = fr(0x100); const contractSlotB = fr(0x150); const contractSlotC = fr(0x200); @@ -389,9 +396,9 @@ describe('public_processor', () => { contractStorageUpdateRequests: [ new ContractStorageUpdateRequest(contractSlotC, fr(0x201), 12, baseContractAddress), ], - }).build(), + }).build(teardownResultSettings), ], - }).build(), + }).build(teardownResultSettings), ]; publicExecutor.simulate.mockImplementation(execution => { @@ -551,6 +558,9 @@ describe('public_processor', () => { publicCallRequests, }); + const teardownGas = tx.data.constants.txContext.gasSettings.getTeardownLimits(); + const teardownResultSettings = { startGasLeft: teardownGas, endGasLeft: teardownGas }; + const contractSlotA = fr(0x100); const contractSlotB = fr(0x150); const contractSlotC = fr(0x200); @@ -588,16 +598,16 @@ describe('public_processor', () => { from: publicCallRequests[1].contractAddress, tx: makeFunctionCall(baseContractAddress, makeSelector(5)), revertReason: new SimulationError('Simulation Failed', []), - }).build(), + }).build(teardownResultSettings), PublicExecutionResultBuilder.fromFunctionCall({ from: publicCallRequests[1].contractAddress, tx: makeFunctionCall(baseContractAddress, makeSelector(5)), contractStorageUpdateRequests: [ new ContractStorageUpdateRequest(contractSlotC, fr(0x201), 14, baseContractAddress), ], - }).build(), + }).build(teardownResultSettings), ], - }).build(), + }).build(teardownResultSettings), ]; publicExecutor.simulate.mockImplementation(execution => { @@ -648,15 +658,44 @@ describe('public_processor', () => { publicCallRequests, }); - // const baseContractAddress = makeAztecAddress(30); + const gasLimits = Gas.from({ l1Gas: 1e9, l2Gas: 1e9, daGas: 1e9 }); + const teardownGas = Gas.from({ l1Gas: 1e7, l2Gas: 1e7, daGas: 1e7 }); + tx.data.constants.txContext.gasSettings = GasSettings.from({ + gasLimits: gasLimits, + teardownGasLimits: teardownGas, + inclusionFee: new Fr(1e4), + maxFeesPerGas: { feePerDaGas: new Fr(10), feePerL1Gas: new Fr(10), feePerL2Gas: new Fr(10) }, + }); + + // Private kernel tail to public pushes teardown gas allocation into revertible gas used + tx.data.forPublic!.end.gasUsed = teardownGas; + tx.data.forPublic!.endNonRevertibleData.gasUsed = Gas.empty(); + const contractSlotA = fr(0x100); const contractSlotB = fr(0x150); const contractSlotC = fr(0x200); let simulatorCallCount = 0; + + const initialGas = gasLimits.sub(teardownGas); + const afterSetupGas = initialGas.sub(Gas.from({ l2Gas: 1e6 })); + const afterAppGas = afterSetupGas.sub(Gas.from({ l2Gas: 2e6, daGas: 2e6 })); + const afterTeardownGas = teardownGas.sub(Gas.from({ l2Gas: 3e6, daGas: 3e6 })); + + // Total gas used is the sum of teardown gas allocation plus all expenditures along the way, + // without including the gas used in the teardown phase (since that's consumed entirely up front). + const expectedTotalGasUsed = { l2Gas: 1e7 + 1e6 + 2e6, daGas: 1e7 + 2e6, l1Gas: 1e7 }; + + // Inclusion fee plus block gas fees times total gas used + const expectedTxFee = 1e4 + (1e7 + 1e6 + 2e6) * 1 + (1e7 + 2e6) * 1 + 1e7 * 1; + const transactionFee = new Fr(expectedTxFee); + const simulatorResults: PublicExecutionResult[] = [ // Setup - PublicExecutionResultBuilder.fromPublicCallRequest({ request: publicCallRequests[0] }).build(), + PublicExecutionResultBuilder.fromPublicCallRequest({ request: publicCallRequests[0] }).build({ + startGasLeft: initialGas, + endGasLeft: afterSetupGas, + }), // App Logic PublicExecutionResultBuilder.fromPublicCallRequest({ @@ -665,7 +704,10 @@ describe('public_processor', () => { new ContractStorageUpdateRequest(contractSlotA, fr(0x101), 14, baseContractAddress), new ContractStorageUpdateRequest(contractSlotB, fr(0x151), 15, baseContractAddress), ], - }).build(), + }).build({ + startGasLeft: afterSetupGas, + endGasLeft: afterAppGas, + }), // Teardown PublicExecutionResultBuilder.fromPublicCallRequest({ @@ -678,21 +720,26 @@ describe('public_processor', () => { new ContractStorageUpdateRequest(contractSlotA, fr(0x101), 11, baseContractAddress), new ContractStorageUpdateRequest(contractSlotC, fr(0x201), 12, baseContractAddress), ], - }).build(), + }).build({ startGasLeft: teardownGas, endGasLeft: teardownGas, transactionFee }), PublicExecutionResultBuilder.fromFunctionCall({ from: publicCallRequests[1].contractAddress, tx: makeFunctionCall(baseContractAddress, makeSelector(5)), contractStorageUpdateRequests: [ new ContractStorageUpdateRequest(contractSlotA, fr(0x102), 13, baseContractAddress), ], - }).build(), + }).build({ startGasLeft: teardownGas, endGasLeft: teardownGas, transactionFee }), ], - }).build(), + }).build({ + startGasLeft: teardownGas, + endGasLeft: afterTeardownGas, + transactionFee, + }), ]; publicExecutor.simulate.mockImplementation(execution => { if (simulatorCallCount < simulatorResults.length) { - return Promise.resolve(simulatorResults[simulatorCallCount++]); + const result = simulatorResults[simulatorCallCount++]; + return Promise.resolve(result); } else { throw new Error(`Unexpected execution request: ${execution}, call count: ${simulatorCallCount}`); } @@ -711,12 +758,28 @@ describe('public_processor', () => { expect(setupSpy).toHaveBeenCalledTimes(1); expect(appLogicSpy).toHaveBeenCalledTimes(1); expect(teardownSpy).toHaveBeenCalledTimes(3); + + const expectedSimulateCall = (availableGas: Partial>, txFee: number) => [ + expect.anything(), // PublicExecution + expect.anything(), // GlobalVariables + Gas.from(availableGas), + expect.anything(), // TxContext + new Fr(txFee), + expect.anything(), // SideEffectCounter + ]; + expect(publicExecutor.simulate).toHaveBeenCalledTimes(3); + expect(publicExecutor.simulate).toHaveBeenNthCalledWith(1, ...expectedSimulateCall(initialGas, 0)); + expect(publicExecutor.simulate).toHaveBeenNthCalledWith(2, ...expectedSimulateCall(afterSetupGas, 0)); + expect(publicExecutor.simulate).toHaveBeenNthCalledWith(3, ...expectedSimulateCall(teardownGas, expectedTxFee)); + expect(publicWorldStateDB.checkpoint).toHaveBeenCalledTimes(3); expect(publicWorldStateDB.rollbackToCheckpoint).toHaveBeenCalledTimes(0); expect(publicWorldStateDB.commit).toHaveBeenCalledTimes(1); expect(publicWorldStateDB.rollbackToCommit).toHaveBeenCalledTimes(0); + expect(processed[0].data.end.gasUsed).toEqual(Gas.from(expectedTotalGasUsed)); + const txEffect = toTxEffect(processed[0]); expect(arrayNonEmptyLength(txEffect.publicDataWrites, PublicDataWrite.isEmpty)).toEqual(3); expect(txEffect.publicDataWrites[0]).toEqual( diff --git a/yarn-project/simulator/src/public/teardown_phase_manager.ts b/yarn-project/simulator/src/public/teardown_phase_manager.ts index c5e84807227..6cec359c9c5 100644 --- a/yarn-project/simulator/src/public/teardown_phase_manager.ts +++ b/yarn-project/simulator/src/public/teardown_phase_manager.ts @@ -1,5 +1,7 @@ import { type PublicKernelRequest, PublicKernelType, type Tx } from '@aztec/circuit-types'; import { + type Fr, + type Gas, type GlobalVariables, type Header, type Proof, @@ -8,6 +10,8 @@ import { import { type PublicExecutor, type PublicStateDB } from '@aztec/simulator'; import { type MerkleTreeOperations } from '@aztec/world-state'; +import { inspect } from 'util'; + import { AbstractPhaseManager, PublicKernelPhase } from './abstract_phase_manager.js'; import { type ContractsDataSourcePublicDB } from './public_executor.js'; import { type PublicKernelCircuitSimulator } from './public_kernel_circuit_simulator.js'; @@ -63,4 +67,18 @@ export class TeardownPhaseManager extends AbstractPhaseManager { returnValues: undefined, }; } + + protected override getTransactionFee(tx: Tx, previousPublicKernelOutput: PublicKernelCircuitPublicInputs): Fr { + const gasSettings = tx.data.constants.txContext.gasSettings; + const gasFees = this.globalVariables.gasFees; + // No need to add teardown limits since they are already included in end.gasUsed + const gasUsed = previousPublicKernelOutput.end.gasUsed.add(previousPublicKernelOutput.endNonRevertibleData.gasUsed); + const txFee = gasSettings.inclusionFee.add(gasUsed.computeFee(gasFees)); + this.log.debug(`Computed tx fee`, { txFee, gasUsed: inspect(gasUsed), gasFees: inspect(gasFees) }); + return txFee; + } + + protected override getAvailableGas(tx: Tx, _previousPublicKernelOutput: PublicKernelCircuitPublicInputs): Gas { + return tx.data.constants.txContext.gasSettings.getTeardownLimits(); + } } diff --git a/yarn-project/simulator/src/public/transitional_adaptors.ts b/yarn-project/simulator/src/public/transitional_adaptors.ts index a3ba62e191f..437d7a4a529 100644 --- a/yarn-project/simulator/src/public/transitional_adaptors.ts +++ b/yarn-project/simulator/src/public/transitional_adaptors.ts @@ -5,7 +5,8 @@ import { ContractStorageRead, ContractStorageUpdateRequest, FunctionData, - GasSettings, + Gas, + type GasSettings, type GlobalVariables, type Header, L2ToL1Message, @@ -37,6 +38,8 @@ export function createAvmExecutionEnvironment( current: PublicExecution, header: Header, globalVariables: GlobalVariables, + gasSettings: GasSettings, + transactionFee: Fr, ): AvmExecutionEnvironment { return new AvmExecutionEnvironment( current.contractAddress, @@ -51,8 +54,8 @@ export function createAvmExecutionEnvironment( current.callContext.isStaticCall, current.callContext.isDelegateCall, current.args, - GasSettings.default(), // TODO(palla/gas): Set proper values - Fr.ZERO, // TODO(palla/gas): Set proper values + gasSettings, + transactionFee, current.functionData.selector, ); } @@ -85,6 +88,9 @@ export function createPublicExecutionContext(avmContext: AvmContext, calldata: F avmContext.persistableState.hostStorage.publicStateDb, avmContext.persistableState.hostStorage.contractsDb, avmContext.persistableState.hostStorage.commitmentsDb, + Gas.from(avmContext.machineState.gasLeft), + avmContext.environment.transactionFee, + avmContext.environment.gasSettings, ); return context; @@ -168,7 +174,9 @@ export async function convertAvmResults( unencryptedLogs, reverted: result.reverted, revertReason: result.revertReason ? createSimulationError(result.revertReason) : undefined, - gasLeft: endMachineState.gasLeft, + startGasLeft: executionContext.availableGas, + endGasLeft: endMachineState.gasLeft, + transactionFee: executionContext.transactionFee, }; }