diff --git a/circuit/environment/src/canary_circuit.rs b/circuit/environment/src/canary_circuit.rs index f49f60c328..a834abea5a 100644 --- a/circuit/environment/src/canary_circuit.rs +++ b/circuit/environment/src/canary_circuit.rs @@ -296,8 +296,9 @@ impl Environment for CanaryCircuit { panic!("{}", &error) } - /// Returns the R1CS circuit, resetting the circuit. - fn inject_r1cs(r1cs: R1CS) { + /// Injects the ejected R1CS circuit and restores the preserved environment state. + fn inject_r1cs(ejected: EjectedR1cs) { + let EjectedR1cs { r1cs, variable_limit, constraint_limit, non_zero_limit, in_witness } = ejected; CANARY_CIRCUIT.with(|circuit| { // Ensure the circuit is empty before injecting. assert_eq!(0, circuit.borrow().num_constants()); @@ -313,19 +314,31 @@ impl Environment for CanaryCircuit { assert_eq!(0, r1cs.num_private()); assert_eq!(1, r1cs.num_variables()); assert_eq!(0, r1cs.num_constraints()); - }) + }); + // Restore the preserved limits. + Self::set_variable_limit(variable_limit); + Self::set_constraint_limit(constraint_limit); + Self::set_non_zero_limit(non_zero_limit); + // Restore the preserved witness mode. + IN_WITNESS.with(|in_witness_cell| in_witness_cell.replace(in_witness)); } - /// Returns the R1CS circuit, resetting the circuit. - fn eject_r1cs_and_reset() -> R1CS { + /// Returns the R1CS circuit and preserved environment state, resetting the circuit. + fn eject_r1cs_and_reset() -> EjectedR1cs { CANARY_CIRCUIT.with(|circuit| { - // Reset the witness mode. - IN_WITNESS.with(|in_witness| in_witness.replace(false)); - // Reset the variable limit. + // Preserve the witness mode. + let in_witness = IN_WITNESS.with(|in_witness| { + let value = in_witness.get(); + in_witness.replace(false); + value + }); + // Preserve the limits. + let variable_limit = Self::get_variable_limit(); + let constraint_limit = Self::get_constraint_limit(); + let non_zero_limit = Self::get_non_zero_limit(); + // Reset the limits for the temporary environment. Self::set_variable_limit(None); - // Reset the constraint limit. Self::set_constraint_limit(None); - // Reset the density limit. Self::set_non_zero_limit(None); // Eject the R1CS instance. let r1cs = circuit.replace(R1CS::<::BaseField>::new()); @@ -335,8 +348,8 @@ impl Environment for CanaryCircuit { assert_eq!(0, circuit.borrow().num_private()); assert_eq!(1, circuit.borrow().num_variables()); assert_eq!(0, circuit.borrow().num_constraints()); - // Return the R1CS instance. - r1cs + // Return the ejected environment state. + EjectedR1cs::new(r1cs, variable_limit, constraint_limit, non_zero_limit, in_witness) }) } diff --git a/circuit/environment/src/circuit.rs b/circuit/environment/src/circuit.rs index b4cd25fdf9..38ba534f5b 100644 --- a/circuit/environment/src/circuit.rs +++ b/circuit/environment/src/circuit.rs @@ -320,8 +320,9 @@ impl Environment for Circuit { panic!("{}", &error) } - /// Returns the R1CS circuit, resetting the circuit. - fn inject_r1cs(r1cs: R1CS) { + /// Injects the ejected R1CS circuit and restores the preserved environment state. + fn inject_r1cs(ejected: EjectedR1cs) { + let EjectedR1cs { r1cs, variable_limit, constraint_limit, non_zero_limit, in_witness } = ejected; CIRCUIT.with(|circuit| { // Ensure the circuit is empty before injecting. assert_eq!(0, circuit.borrow().num_constants()); @@ -337,19 +338,31 @@ impl Environment for Circuit { assert_eq!(0, r1cs.num_private()); assert_eq!(1, r1cs.num_variables()); assert_eq!(0, r1cs.num_constraints()); - }) + }); + // Restore the preserved limits. + Self::set_variable_limit(variable_limit); + Self::set_constraint_limit(constraint_limit); + Self::set_non_zero_limit(non_zero_limit); + // Restore the preserved witness mode. + IN_WITNESS.with(|in_witness_cell| in_witness_cell.replace(in_witness)); } - /// Returns the R1CS circuit, resetting the circuit. - fn eject_r1cs_and_reset() -> R1CS { + /// Returns the R1CS circuit and preserved environment state, resetting the circuit. + fn eject_r1cs_and_reset() -> EjectedR1cs { CIRCUIT.with(|circuit| { - // Reset the witness mode. - IN_WITNESS.with(|in_witness| in_witness.replace(false)); - // Reset the variable limit. + // Preserve the witness mode. + let in_witness = IN_WITNESS.with(|in_witness| { + let value = in_witness.get(); + in_witness.replace(false); + value + }); + // Preserve the limits. + let variable_limit = Self::get_variable_limit(); + let constraint_limit = Self::get_constraint_limit(); + let non_zero_limit = Self::get_non_zero_limit(); + // Reset the limits for the temporary environment. Self::set_variable_limit(None); - // Reset the constraint limit. Self::set_constraint_limit(None); - // Reset the density limit. Self::set_non_zero_limit(None); // Eject the R1CS instance. let r1cs = circuit.replace(R1CS::<::BaseField>::new()); @@ -359,8 +372,8 @@ impl Environment for Circuit { assert_eq!(0, circuit.borrow().num_private()); assert_eq!(1, circuit.borrow().num_variables()); assert_eq!(0, circuit.borrow().num_constraints()); - // Return the R1CS instance. - r1cs + // Return the ejected environment state. + EjectedR1cs::new(r1cs, variable_limit, constraint_limit, non_zero_limit, in_witness) }) } @@ -464,4 +477,46 @@ mod tests { assert_eq!(0, Circuit::num_constraints_in_scope()); }) } + + #[test] + fn test_eject_inject_preserves_limits() { + use std::panic::{self, AssertUnwindSafe}; + + /// Compute 2^EXPONENT - 1, in a purposefully constraint-inefficient manner for testing. + fn add_constraints(exponent: u64) { + let one = snarkvm_console_types::Field::<::Network>::one(); + let two = one + one; + + let mut candidate = Field::::new(Mode::Public, one); + let mut accumulator = Field::::new(Mode::Private, two); + for _ in 0..exponent { + candidate += &accumulator; + accumulator *= Field::::new(Mode::Private, two); + } + } + + Circuit::reset(); + Circuit::set_constraint_limit(Some(20)); + + add_constraints::(10); + assert_eq!(10, Circuit::num_constraints()); + + let ejected = Circuit::eject_r1cs_and_reset(); + assert_eq!(None, Circuit::get_constraint_limit()); + assert_eq!(Some(20), ejected.constraint_limit()); + + // Without limits, the temporary environment can synthesize beyond the preserved limit. + add_constraints::(25); + assert_eq!(25, Circuit::num_constraints()); + + // Nested execution resets the circuit before returning control to the caller. + Circuit::reset(); + + Circuit::inject_r1cs(ejected); + assert_eq!(Some(20), Circuit::get_constraint_limit()); + assert_eq!(10, Circuit::num_constraints()); + + let result = panic::catch_unwind(AssertUnwindSafe(|| add_constraints::(12))); + assert!(result.is_err()); + } } diff --git a/circuit/environment/src/environment.rs b/circuit/environment/src/environment.rs index cdcced6736..588b04bf59 100644 --- a/circuit/environment/src/environment.rs +++ b/circuit/environment/src/environment.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{Assignment, ConstraintUnsatisfied, Inject, LinearCombination, Mode, R1CS, Variable, witness_mode}; +use crate::{Assignment, ConstraintUnsatisfied, EjectedR1cs, Inject, LinearCombination, Mode, Variable, witness_mode}; use snarkvm_curves::AffineCurve; use snarkvm_fields::traits::*; @@ -185,11 +185,11 @@ pub trait Environment: 'static + Copy + Clone + fmt::Debug + fmt::Display + Eq + ::halt(message) } - /// Returns the R1CS circuit, resetting the circuit. - fn inject_r1cs(r1cs: R1CS); + /// Injects the ejected R1CS circuit and restores the preserved environment state. + fn inject_r1cs(ejected: EjectedR1cs); - /// Returns the R1CS circuit, resetting the circuit. - fn eject_r1cs_and_reset() -> R1CS; + /// Returns the R1CS circuit and preserved environment state, resetting the circuit. + fn eject_r1cs_and_reset() -> EjectedR1cs; /// Returns the R1CS assignment of the circuit, resetting the circuit. fn eject_assignment_and_reset() -> Assignment<::Field>; diff --git a/circuit/environment/src/helpers/r1cs.rs b/circuit/environment/src/helpers/r1cs.rs index 627f2949ce..6617140c83 100644 --- a/circuit/environment/src/helpers/r1cs.rs +++ b/circuit/environment/src/helpers/r1cs.rs @@ -266,3 +266,50 @@ impl Display for R1CS { write!(f, "{output}") } } + +/// Captures the circuit environment state preserved across a temporary R1CS ejection. +#[derive(Debug)] +pub struct EjectedR1cs { + pub(crate) r1cs: R1CS, + pub(crate) variable_limit: Option, + pub(crate) constraint_limit: Option, + pub(crate) non_zero_limit: Option<(u64, u64, u64)>, + pub(crate) in_witness: bool, +} + +impl EjectedR1cs { + /// Returns the ejected R1CS without restoring the environment state. + pub fn into_r1cs(self) -> R1CS { + self.r1cs + } + + /// Returns the preserved variable limit. + pub const fn variable_limit(&self) -> Option { + self.variable_limit + } + + /// Returns the preserved constraint limit. + pub const fn constraint_limit(&self) -> Option { + self.constraint_limit + } + + /// Returns the preserved non-zero limit. + pub const fn non_zero_limit(&self) -> Option<(u64, u64, u64)> { + self.non_zero_limit + } + + /// Returns the preserved witness mode. + pub const fn in_witness(&self) -> bool { + self.in_witness + } + + pub(crate) fn new( + r1cs: R1CS, + variable_limit: Option, + constraint_limit: Option, + non_zero_limit: Option<(u64, u64, u64)>, + in_witness: bool, + ) -> Self { + Self { r1cs, variable_limit, constraint_limit, non_zero_limit, in_witness } + } +} diff --git a/circuit/environment/src/testnet_circuit.rs b/circuit/environment/src/testnet_circuit.rs index 5e309dfcf4..66c1ffd57f 100644 --- a/circuit/environment/src/testnet_circuit.rs +++ b/circuit/environment/src/testnet_circuit.rs @@ -296,8 +296,9 @@ impl Environment for TestnetCircuit { panic!("{}", &error) } - /// Returns the R1CS circuit, resetting the circuit. - fn inject_r1cs(r1cs: R1CS) { + /// Injects the ejected R1CS circuit and restores the preserved environment state. + fn inject_r1cs(ejected: EjectedR1cs) { + let EjectedR1cs { r1cs, variable_limit, constraint_limit, non_zero_limit, in_witness } = ejected; TESTNET_CIRCUIT.with(|circuit| { // Ensure the circuit is empty before injecting. assert_eq!(0, circuit.borrow().num_constants()); @@ -313,19 +314,31 @@ impl Environment for TestnetCircuit { assert_eq!(0, r1cs.num_private()); assert_eq!(1, r1cs.num_variables()); assert_eq!(0, r1cs.num_constraints()); - }) + }); + // Restore the preserved limits. + Self::set_variable_limit(variable_limit); + Self::set_constraint_limit(constraint_limit); + Self::set_non_zero_limit(non_zero_limit); + // Restore the preserved witness mode. + IN_WITNESS.with(|in_witness_cell| in_witness_cell.replace(in_witness)); } - /// Returns the R1CS circuit, resetting the circuit. - fn eject_r1cs_and_reset() -> R1CS { + /// Returns the R1CS circuit and preserved environment state, resetting the circuit. + fn eject_r1cs_and_reset() -> EjectedR1cs { TESTNET_CIRCUIT.with(|circuit| { - // Reset the witness mode. - IN_WITNESS.with(|in_witness| in_witness.replace(false)); - // Reset the variable limit. + // Preserve the witness mode. + let in_witness = IN_WITNESS.with(|in_witness| { + let value = in_witness.get(); + in_witness.replace(false); + value + }); + // Preserve the limits. + let variable_limit = Self::get_variable_limit(); + let constraint_limit = Self::get_constraint_limit(); + let non_zero_limit = Self::get_non_zero_limit(); + // Reset the limits for the temporary environment. Self::set_variable_limit(None); - // Reset the constraint limit. Self::set_constraint_limit(None); - // Reset the density limit. Self::set_non_zero_limit(None); // Eject the R1CS instance. let r1cs = circuit.replace(R1CS::<::BaseField>::new()); @@ -335,8 +348,8 @@ impl Environment for TestnetCircuit { assert_eq!(0, circuit.borrow().num_private()); assert_eq!(1, circuit.borrow().num_variables()); assert_eq!(0, circuit.borrow().num_constraints()); - // Return the R1CS instance. - r1cs + // Return the ejected environment state. + EjectedR1cs::new(r1cs, variable_limit, constraint_limit, non_zero_limit, in_witness) }) } diff --git a/circuit/network/src/canary_v0.rs b/circuit/network/src/canary_v0.rs index bc857678a9..e728bb6217 100644 --- a/circuit/network/src/canary_v0.rs +++ b/circuit/network/src/canary_v0.rs @@ -44,7 +44,7 @@ use snarkvm_circuit_types::{ Field, Group, Scalar, - environment::{Assignment, CanaryCircuit, R1CS, prelude::*}, + environment::{Assignment, CanaryCircuit, EjectedR1cs, prelude::*}, }; use core::fmt; @@ -538,13 +538,13 @@ impl Environment for AleoCanaryV0 { E::halt(message) } - /// Returns the R1CS circuit, resetting the circuit. - fn inject_r1cs(r1cs: R1CS) { - E::inject_r1cs(r1cs) + /// Injects the ejected R1CS circuit and restores the preserved environment state. + fn inject_r1cs(ejected: EjectedR1cs) { + E::inject_r1cs(ejected) } - /// Returns the R1CS circuit, resetting the circuit. - fn eject_r1cs_and_reset() -> R1CS { + /// Returns the R1CS circuit and preserved environment state, resetting the circuit. + fn eject_r1cs_and_reset() -> EjectedR1cs { E::eject_r1cs_and_reset() } diff --git a/circuit/network/src/testnet_v0.rs b/circuit/network/src/testnet_v0.rs index 68f718ff31..80b23ef63e 100644 --- a/circuit/network/src/testnet_v0.rs +++ b/circuit/network/src/testnet_v0.rs @@ -44,7 +44,7 @@ use snarkvm_circuit_types::{ Field, Group, Scalar, - environment::{Assignment, R1CS, TestnetCircuit, prelude::*}, + environment::{Assignment, EjectedR1cs, TestnetCircuit, prelude::*}, }; use core::fmt; @@ -538,13 +538,13 @@ impl Environment for AleoTestnetV0 { E::halt(message) } - /// Returns the R1CS circuit, resetting the circuit. - fn inject_r1cs(r1cs: R1CS) { - E::inject_r1cs(r1cs) + /// Injects the ejected R1CS circuit and restores the preserved environment state. + fn inject_r1cs(ejected: EjectedR1cs) { + E::inject_r1cs(ejected) } - /// Returns the R1CS circuit, resetting the circuit. - fn eject_r1cs_and_reset() -> R1CS { + /// Returns the R1CS circuit and preserved environment state, resetting the circuit. + fn eject_r1cs_and_reset() -> EjectedR1cs { E::eject_r1cs_and_reset() } diff --git a/circuit/network/src/v0.rs b/circuit/network/src/v0.rs index ec204d8872..60e9f7cf69 100644 --- a/circuit/network/src/v0.rs +++ b/circuit/network/src/v0.rs @@ -44,7 +44,7 @@ use snarkvm_circuit_types::{ Field, Group, Scalar, - environment::{Assignment, Circuit, R1CS, prelude::*}, + environment::{Assignment, Circuit, EjectedR1cs, prelude::*}, }; use core::fmt; @@ -538,13 +538,13 @@ impl Environment for AleoV0 { E::halt(message) } - /// Returns the R1CS circuit, resetting the circuit. - fn inject_r1cs(r1cs: R1CS) { - E::inject_r1cs(r1cs) + /// Injects the ejected R1CS circuit and restores the preserved environment state. + fn inject_r1cs(ejected: EjectedR1cs) { + E::inject_r1cs(ejected) } - /// Returns the R1CS circuit, resetting the circuit. - fn eject_r1cs_and_reset() -> R1CS { + /// Returns the R1CS circuit and preserved environment state, resetting the circuit. + fn eject_r1cs_and_reset() -> EjectedR1cs { E::eject_r1cs_and_reset() } diff --git a/ledger/puzzle/epoch/src/synthesis/program/to_r1cs.rs b/ledger/puzzle/epoch/src/synthesis/program/to_r1cs.rs index 425bd19749..76ad17669a 100644 --- a/ledger/puzzle/epoch/src/synthesis/program/to_r1cs.rs +++ b/ledger/puzzle/epoch/src/synthesis/program/to_r1cs.rs @@ -95,7 +95,7 @@ impl EpochProgram { lap!(timer, "Ensure the circuit is satisfied"); // Eject the R1CS and reset the circuit. - let r1cs = A::eject_r1cs_and_reset(); + let r1cs = A::eject_r1cs_and_reset().into_r1cs(); finish!(timer, "Eject the circuit assignment and reset the circuit"); Ok(r1cs) diff --git a/synthesizer/process/src/stack/execute.rs b/synthesizer/process/src/stack/execute.rs index cd0545cd52..4b2f0d437c 100644 --- a/synthesizer/process/src/stack/execute.rs +++ b/synthesizer/process/src/stack/execute.rs @@ -210,7 +210,6 @@ impl Stack { A::reset(); // If in 'CheckDeployment' mode, set the constraint limit and variable limit. - // We do not have to reset it after function calls because `CheckDeployment` mode does not execute those. if let CallStack::CheckDeployment(_, _, _, constraint_limit, variable_limit, non_zero_limit) = &call_stack { A::set_constraint_limit(*constraint_limit); A::set_variable_limit(*variable_limit); diff --git a/synthesizer/process/src/tests/test_execute.rs b/synthesizer/process/src/tests/test_execute.rs index ffda69f964..a96813bb56 100644 --- a/synthesizer/process/src/tests/test_execute.rs +++ b/synthesizer/process/src/tests/test_execute.rs @@ -13,7 +13,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{CallStack, InclusionVersion, Process, Trace, execution_cost, execution_cost_for_authorization}; +use crate::{ + Assignments, + CallStack, + InclusionVersion, + Process, + Request, + Stack, + Trace, + execution_cost, + execution_cost_for_authorization, +}; use circuit::{Aleo, network::AleoV0}; use console::{ account::{Address, PrivateKey, ViewKey}, @@ -2985,3 +2995,236 @@ fn test_program_exceeding_transaction_spend_limit() { // Attempt to verify the deployment, which should fail. assert!(process.verify_deployment::(ConsensusVersion::V8, &deployment, rng).is_ok()); } + +/// Returns the number of constraints synthesized in `CheckDeployment` mode. +fn check_deployment_num_constraints( + stack: &Stack, + private_key: &PrivateKey, + function_name: Identifier, + inputs: &[Value], + constraint_limit: Option, + variable_limit: Option, + non_zero_limit: Option<(u64, u64, u64)>, + rng: &mut TestRng, +) -> Result { + use snarkvm_synthesizer_program::StackTrait; + use std::panic::{self, AssertUnwindSafe}; + + let program = stack.program(); + let program_id = *program.id(); + let input_types = program.get_function(&function_name)?.input_types(); + let program_checksum = match stack.program().contains_constructor() { + true => Some(stack.program_checksum_as_field()?), + false => None, + }; + let request = Request::sign( + private_key, + program_id, + function_name, + inputs.iter(), + &input_types, + None, + true, + program_checksum, + false, + rng, + )?; + let assignments = Assignments::::default(); + let call_stack = CallStack::CheckDeployment( + vec![request], + *private_key, + assignments.clone(), + constraint_limit, + variable_limit, + non_zero_limit, + ); + + match panic::catch_unwind(AssertUnwindSafe(|| { + stack.execute_function::(call_stack, None, None, rng) + })) { + Ok(Ok(_)) => Ok(assignments.read().last().unwrap().0.num_constraints()), + Ok(Err(err)) => bail!("{err}"), + Err(payload) => { + if let Some(message) = payload.downcast_ref::<&str>() { + bail!("{message}"); + } else if let Some(message) = payload.downcast_ref::() { + bail!("{message}"); + } + bail!("Synthesis halted"); + } + } +} + +/// Builds a suffix of repeated field additions for synthesis limit tests. +fn field_add_suffix(num_adds: usize) -> String { + let mut suffix = String::from("add r1 r1 into r2;\n"); + for i in 2..=num_adds { + suffix.push_str(&format!("add r{i} r{i} into r{};\n", i + 1)); + } + suffix.push_str(&format!("output r{} as field.private;", num_adds + 1)); + suffix +} + +/// Initializes a process containing the given programs. +fn process_with_programs(programs: &[&str]) -> Process { + let process = Process::::load().unwrap(); + for source in programs { + let (_, program) = Program::::parse(source).unwrap(); + process.lock().add_program(&program).unwrap(); + } + process +} + +#[test] +fn test_check_deployment_static_call_enforces_constraint_limit_after_call() { + let rng = &mut TestRng::default(); + let private_key = PrivateKey::::new(rng).unwrap(); + let input = Value::::Plaintext(Plaintext::from_str("3field").unwrap()); + + let callee = r" +program callee_limits.aleo; +function noop: + input r0 as field.private; + output r0 as field.private;"; + + let caller_prefix = r" +import callee_limits.aleo; +program caller_prefix.aleo; +function compute: + input r0 as field.private; + call callee_limits.aleo/noop r0 into r1; + output r1 as field.private;"; + + let caller_full = format!( + r" +import callee_limits.aleo; +program caller_full.aleo; +function compute: + input r0 as field.private; + call callee_limits.aleo/noop r0 into r1; + {}", + field_add_suffix(32), + ); + + let process = process_with_programs(&[callee, caller_prefix, &caller_full]); + let prefix_stack = process.get_stack(ProgramID::from_str("caller_prefix.aleo").unwrap()).unwrap(); + let full_stack = process.get_stack(ProgramID::from_str("caller_full.aleo").unwrap()).unwrap(); + let function_name = Identifier::from_str("compute").unwrap(); + + let prefix_constraints = check_deployment_num_constraints( + &prefix_stack, + &private_key, + function_name, + &[input.clone()], + None, + None, + None, + rng, + ) + .unwrap(); + let full_constraints = check_deployment_num_constraints( + &full_stack, + &private_key, + function_name, + &[input.clone()], + None, + None, + None, + rng, + ) + .unwrap(); + assert!(prefix_constraints < full_constraints); + + let constraint_limit = prefix_constraints + (full_constraints - prefix_constraints) / 2; + let result = check_deployment_num_constraints( + &full_stack, + &private_key, + function_name, + &[input], + Some(constraint_limit), + None, + None, + rng, + ); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Surpassed the constraint limit")); +} + +#[test] +fn test_check_deployment_dynamic_call_enforces_constraint_limit_after_call() { + let rng = &mut TestRng::default(); + let private_key = PrivateKey::::new(rng).unwrap(); + let input = Value::::Plaintext(Plaintext::from_str("3field").unwrap()); + + let network_field = Identifier::::from_str("aleo").unwrap().to_field().unwrap(); + let callee_prog_field = Identifier::::from_str("dyn_callee").unwrap().to_field().unwrap(); + let callee_fn_field = Identifier::::from_str("noop").unwrap().to_field().unwrap(); + + let callee = r" +program dyn_callee.aleo; +function noop: + input r0 as field.private; + output r0 as field.private;"; + + let caller_prefix = format!( + r" +program dyn_caller_prefix.aleo; +function compute: + input r0 as field.private; + call.dynamic {callee_prog_field} {network_field} {callee_fn_field} with r0 (as field.private) into r1 (as field.private); + output r1 as field.private;" + ); + + let caller_full = format!( + r" +program dyn_caller_full.aleo; +function compute: + input r0 as field.private; + call.dynamic {callee_prog_field} {network_field} {callee_fn_field} with r0 (as field.private) into r1 (as field.private); + {}", + field_add_suffix(32), + ); + + let process = process_with_programs(&[callee, &caller_prefix, &caller_full]); + let prefix_stack = process.get_stack(ProgramID::from_str("dyn_caller_prefix.aleo").unwrap()).unwrap(); + let full_stack = process.get_stack(ProgramID::from_str("dyn_caller_full.aleo").unwrap()).unwrap(); + let function_name = Identifier::from_str("compute").unwrap(); + + let prefix_constraints = check_deployment_num_constraints( + &prefix_stack, + &private_key, + function_name, + &[input.clone()], + None, + None, + None, + rng, + ) + .unwrap(); + let full_constraints = check_deployment_num_constraints( + &full_stack, + &private_key, + function_name, + &[input.clone()], + None, + None, + None, + rng, + ) + .unwrap(); + assert!(prefix_constraints < full_constraints); + + let constraint_limit = prefix_constraints + (full_constraints - prefix_constraints) / 2; + let result = check_deployment_num_constraints( + &full_stack, + &private_key, + function_name, + &[input], + Some(constraint_limit), + None, + None, + rng, + ); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Surpassed the constraint limit")); +}