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 d25e42f490a..1746a799f4b 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 @@ -254,12 +254,19 @@ pub fn update_non_revertible_gas_used(public_call: PublicCallData, circuit_outpu // 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" - ); + // If this is a nested call (not an execution request), the start gas is correct as long as the + // call being processed by this kernel iteration matches the call at the top of the callstack + // as per the previous kernel's outputs. + // An execution request's start gas is the remaining gas left in the transaction after the previous kernel. + // A nested call's start gas is the gas allocated to it by its caller and placed in the callstack. + if (public_call.call_stack_item.is_execution_request) { + 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 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 d6123c39ac6..ddfd090a546 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 @@ -475,6 +475,7 @@ mod tests { fn validates_start_gas() { let mut builder = PublicKernelAppLogicCircuitPrivateInputsBuilder::new(); + builder.public_call.is_execution_request = true; // don't need to check start gas for nested calls builder.public_call.public_inputs.start_gas_left = Gas::new(100, 100); 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 65a8ed28b68..57baeb5d851 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 @@ -531,6 +531,7 @@ mod tests { fn validates_start_gas() { let mut builder = PublicKernelSetupCircuitPrivateInputsBuilder::new(); + builder.public_call.is_execution_request = true; // don't need to check start gas for nested calls builder.public_call.public_inputs.start_gas_left = Gas::new(100, 100); 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 6c877b458e7..67174294853 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 @@ -24,11 +24,18 @@ impl PublicKernelTeardownCircuitPrivateInputs { // 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" - ); + // If this is a nested call (not an execution request), the start gas is correct as long as the + // call being processed by this kernel iteration matches the call at the top of the callstack + // as per the previous kernel's outputs. + // An execution request's start gas is the remaining gas left in the transaction after the previous kernel. + // A nested call's start gas is the gas allocated to it by its caller and placed in the callstack. + if (self.public_call.call_stack_item.is_execution_request) { + 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 @@ -413,6 +420,8 @@ mod tests { #[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.is_execution_request = true; // don't need to check start gas for nested calls builder.public_call.public_inputs.start_gas_left = Gas::new(10, 30); builder.failed();