snarkvm_synthesizer_process/verify_execution/mod.rs
1// Copyright (c) 2019-2026 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use super::*;
17
18mod ensure_records_exist;
19
20impl<N: Network> Process<N> {
21 /// Verifies the given execution is valid.
22 /// Note: This does *not* check that the global state root exists in the ledger.
23 #[inline]
24 pub fn verify_execution(
25 consensus_version: ConsensusVersion,
26 varuna_version: VarunaVersion,
27 inclusion_version: InclusionVersion,
28 execution: &Execution<N>,
29 execution_stacks: &IndexMap<ProgramID<N>, Arc<Stack<N>>>,
30 ) -> Result<()> {
31 let timer = timer!("Process::verify_execution");
32
33 // Ensure the execution contains transitions.
34 ensure!(!execution.is_empty(), "There are no transitions in the execution");
35 // Ensure that the execution does not exceed the maximum number of transitions.
36 ensure!(
37 execution.len() < Transaction::<N>::MAX_TRANSITIONS,
38 "The number of transitions in an execution must be less than '{}'",
39 Transaction::<N>::MAX_TRANSITIONS
40 );
41
42 // Determine the function locator and ensure the number of transitions matches the number of calls.
43 let locator = {
44 // Retrieve the transition (without popping it).
45 let transition = execution.peek()?;
46 // Retrieve the stack.
47 let stack = execution_stacks
48 .get(transition.program_id())
49 .ok_or_else(|| anyhow!("Missing stack for program '{}'", transition.program_id()))?;
50 // Calculate the minimum number of calls for the root transition.
51 let minimum_number_of_calls = stack.get_minimum_number_of_calls(transition.function_name())?;
52 // If the root transition contains a dynamic call,
53 // - ensure that the number of calls is less than or equal to the number of transitions.
54 // - otherwise, ensure that the number of calls matches the number of transitions.
55 match stack.contains_dynamic_call(transition.function_name())? {
56 true => ensure!(
57 minimum_number_of_calls <= execution.len(),
58 "The number of transitions in the execution is incorrect. Expected at least {minimum_number_of_calls}, but found {}",
59 execution.len()
60 ),
61 false => ensure!(
62 minimum_number_of_calls == execution.len(),
63 "The number of transitions in the execution is incorrect. Expected {minimum_number_of_calls}, but found {}",
64 execution.len()
65 ),
66 }
67
68 // Output the locator of the main function.
69 Locator::new(*transition.program_id(), *transition.function_name()).to_string()
70 };
71 lap!(timer, "Verify the number of transitions");
72
73 // Construct the call graph of the execution.
74 let call_graph = Self::construct_call_graph(execution.transitions(), execution_stacks)?;
75
76 // From ConsensusVersion::V15 onwards, ensure that, for each non-closure
77 // function in the execution, all DynamicRecords and ExternalRecords
78 // received as inputs or from callees exist on the ledger at the end of
79 // the execution (whether spent or not).
80 if consensus_version >= ConsensusVersion::V15 {
81 Self::ensure_records_exist(execution.transitions(), &call_graph, execution_stacks)?;
82 }
83
84 // Construct the reverse call graph of the execution.
85 // Note: This is a mapping of the child transition ID to the parent transition ID.
86 let reverse_call_graph = Self::reverse_call_graph(&call_graph);
87
88 // Initialize a map of verifying keys to public inputs.
89 let mut verifier_inputs = HashMap::with_capacity(execution.transitions().len());
90
91 // Initialize a map of transition IDs to references of the transition.
92 let mut transition_map = HashMap::with_capacity(execution.transitions().len());
93
94 // Retrieve the network ID.
95 let network_id = U16::new(N::ID);
96
97 // Cache function IDs keyed by (program ID, function name) to avoid redundant BHP hashing.
98 // Computed on demand and cached for reuse across transitions that call the same function.
99 let mut function_id_cache: HashMap<(ProgramID<N>, Identifier<N>), Field<N>> =
100 HashMap::with_capacity(execution.transitions().len());
101
102 // Verify each transition.
103 for transition in execution.transitions() {
104 dev_println!("Verifying transition for {}/{}...", transition.program_id(), transition.function_name());
105 // Debug-mode only, as the `Transition` constructor recomputes the transition ID at initialization.
106 #[cfg(debug_assertions)]
107 {
108 let expected_id = N::hash_bhp512(&(transition.to_root()?, *transition.tcm()).to_bits_le())?;
109 assert_eq!(**transition.id(), expected_id, "The transition ID is incorrect");
110 }
111
112 // Ensure the transition is not a fee transition.
113 let is_fee_transition = transition.is_fee_private() || transition.is_fee_public();
114 ensure!(!is_fee_transition, "Fee transitions are not allowed in executions");
115 // Ensure the number of inputs is within the allowed range.
116 ensure!(transition.inputs().len() <= N::MAX_INPUTS, "Transition exceeded maximum number of inputs");
117 // Ensure the number of outputs is within the allowed range.
118 ensure!(transition.outputs().len() <= N::MAX_OUTPUTS, "Transition exceeded maximum number of outputs");
119
120 // Retrieve (or compute and cache) the function ID for this transition.
121 let function_id = get_or_compute_function_id(
122 &mut function_id_cache,
123 &network_id,
124 transition.program_id(),
125 transition.function_name(),
126 )?;
127
128 // Ensure each input is valid.
129 if transition
130 .inputs()
131 .iter()
132 .enumerate()
133 .any(|(index, input)| !input.verify(function_id, transition.tcm(), index))
134 {
135 bail!("Failed to verify a transition input")
136 }
137 lap!(timer, "Verify the inputs");
138
139 // Ensure each output is valid.
140 let num_inputs = transition.inputs().len();
141 let num_outputs = transition.outputs().len();
142 for (index, output) in transition.outputs().iter().enumerate() {
143 // If the consensus version are before `ConsensusVersion::V8`, ensure the output record is on Version 0.
144 // if the consensus version is on or after `ConsensusVersion::V8`, ensure the output record is on Version 1.
145 if let Some((_, record)) = output.record() {
146 if (ConsensusVersion::V1..=ConsensusVersion::V7).contains(&consensus_version) {
147 #[cfg(not(any(test, feature = "test")))]
148 ensure!(record.version().is_zero(), "Output record must be Version 0 before Consensus V8");
149 #[cfg(any(test, feature = "test"))]
150 ensure!(
151 record.version().is_one(),
152 "Output record must be Version 1 before Consensus V8 in tests."
153 );
154 } else {
155 ensure!(record.version().is_one(), "Output record must be Version 1 on or after Consensus V8");
156 }
157 }
158 // Ensure the output is valid.
159 if !output.verify(function_id, transition.tcm(), num_inputs + index) {
160 bail!("Failed to verify a transition output")
161 }
162 }
163 lap!(timer, "Verify the outputs");
164
165 // Retrieve the stack.
166 let stack = execution_stacks
167 .get(transition.program_id())
168 .ok_or_else(|| anyhow!("Missing stack for program '{}'", transition.program_id()))?;
169 // Retrieve the function from the stack.
170 let function = stack.get_function(transition.function_name())?;
171 // Retrieve the program checksum, if the program has a constructor.
172 let program_checksum = match stack.program().contains_constructor() {
173 true => Some(stack.program_checksum_as_field()?),
174 false => None,
175 };
176
177 // Ensure the number of inputs and outputs match the expected number in the function.
178 ensure!(function.inputs().len() == num_inputs, "The number of transition inputs is incorrect");
179 ensure!(function.outputs().len() == num_outputs, "The number of transition outputs is incorrect");
180
181 // Ensure the input and output types are equivalent to the ones defined in the function.
182 // We only need to check that the variant type matches because we already check the hashes in
183 // the `Input::verify` and `Output::verify` functions.
184 // Note: The length checks above already verify that the counts match,
185 // so `zip_eq` here is safe and acts as a defense-in-depth assertion.
186 for (function_input, transition_input) in function.input_types().iter().zip_eq(transition.inputs().iter()) {
187 ensure!(
188 transition_input.is_type(function_input),
189 "Input variant mismatch: expected '{function_input}', found '{transition_input}'",
190 );
191 }
192 for (output_index, (function_output, transition_output)) in
193 function.output_types().iter().zip_eq(transition.outputs().iter()).enumerate()
194 {
195 ensure!(
196 transition_output.is_type(function_output),
197 "Output variant mismatch at index {output_index} in '{}/{}': expected '{function_output}', found '{transition_output}'",
198 transition.program_id(),
199 transition.function_name(),
200 );
201 }
202
203 // Retrieve the parent program ID.
204 // Note: The last transition in the execution does not have a parent, by definition.
205 let parent = reverse_call_graph.get(transition.id()).and_then(|tid| execution.get_program_id(tid));
206
207 // Add the transition to the transition map.
208 transition_map.insert(*transition.id(), (transition, function.clone()));
209
210 // Construct the verifier inputs for the transition.
211 let inputs = Self::to_transition_verifier_inputs(
212 transition,
213 parent,
214 &call_graph,
215 program_checksum.map(|checksum| *checksum),
216 &transition_map,
217 &mut function_id_cache,
218 &network_id,
219 execution_stacks,
220 )?;
221 lap!(timer, "Constructed the verifier inputs for a transition of {}", function.name());
222
223 // Save the verifying key and its inputs.
224 verifier_inputs
225 .entry(Locator::new(*stack.program_id(), *function.name()))
226 // Retrieve the verifying key, if it does not already exist.
227 .or_insert((stack.get_verifying_key(function.name())?, vec![]))
228 .1
229 .push(inputs);
230 lap!(timer, "Stored the verifier inputs for a transition of {}", function.name());
231 }
232
233 // Sanity check: each transition must produce exactly one verifier instance.
234 let num_instances = verifier_inputs.values().map(|(_, inputs)| inputs.len()).sum::<usize>();
235 ensure!(num_instances == execution.transitions().len(), "The number of verifier instances is incorrect");
236 // Ensure the same signer is used for all transitions.
237 execution.transitions().try_fold(None, |signer, transition| {
238 Ok(match signer {
239 None => Some(transition.scm()),
240 Some(signer) => {
241 ensure!(signer == transition.scm(), "The transitions did not use the same signer");
242 Some(signer)
243 }
244 })
245 })?;
246
247 // Construct the list of verifier inputs.
248 let mut verifier_inputs: Vec<_> = verifier_inputs.values().cloned().collect();
249
250 // Construct the batch of translation verifier inputs.
251 let batch_translation_inputs = Translation::prepare_verifier_inputs(
252 execution.transitions(),
253 &transition_map,
254 &|(program_id, record_name)| {
255 execution_stacks
256 .get(program_id)
257 .ok_or_else(|| anyhow!("Missing stack for program '{program_id}'"))
258 .and_then(|stack| stack.get_verifying_key(record_name))
259 },
260 )?;
261 // Sanity check: the number of translation inputs computed here must match the count
262 // derived from the authorization. We only compare totals because
263 // `Authorization::translation_batch_sizes` does not preserve order; the prover and
264 // verifier agree on order through the use of `translation_index`.
265 let expected_n_translations =
266 Authorization::translation_batch_sizes(execution.transitions(), execution_stacks)?
267 .into_iter()
268 .sum::<usize>();
269 let actual_n_translations = batch_translation_inputs.iter().map(|(_, inputs)| inputs.len()).sum::<usize>();
270 ensure!(
271 actual_n_translations == expected_n_translations,
272 "Unexpected number of translation inputs: {actual_n_translations} instead of {expected_n_translations}",
273 );
274 for (verifying_key, batch_translation_inputs_for_record) in batch_translation_inputs.into_iter() {
275 // Insert the translation verifier inputs.
276 verifier_inputs.push((verifying_key.clone(), batch_translation_inputs_for_record));
277 }
278
279 // Enforce the batch proof instance limit starting from ConsensusVersion::V14,
280 // which introduces translation proofs that increase the total instance count.
281 // Note: This check is performed here (rather than in `VerifyingKey::verify_batch`)
282 // because the consensus version is only available at this level. The total instance
283 // count includes transition, translation, and inclusion proof instances; the inclusion
284 // instances are added inside `verify_execution_proof`, but their count is bounded by
285 // the number of record inputs which is already constrained by `MAX_INPUTS * MAX_TRANSITIONS`.
286 if consensus_version >= ConsensusVersion::V14 {
287 let num_instances = verifier_inputs.iter().map(|(_, inputs)| inputs.len()).sum::<usize>();
288 // Account for inclusion proof instances (one per record input across all transitions).
289 let num_inclusion_instances = Authorization::number_of_input_records(execution.transitions());
290 let total_instances = num_instances + num_inclusion_instances;
291 ensure!(
292 total_instances <= N::MAX_BATCH_PROOF_INSTANCES,
293 "Observed {total_instances} instances to verify, the limit is {}",
294 N::MAX_BATCH_PROOF_INSTANCES
295 );
296 }
297
298 // Sanity check: each public input vector must not exceed the verifying key's expected input
299 // count. The Varuna verifier pads inputs up to the domain size (the next power of two at
300 // least as large as `num_public_inputs`) with zero field elements, so having fewer inputs
301 // than the padded count is always valid.
302 #[cfg(not(feature = "dev_skip_checks"))]
303 {
304 for (verifying_key, inputs_list) in &verifier_inputs {
305 let expected = verifying_key.circuit_info.num_public_inputs;
306 for inputs in inputs_list {
307 ensure!(
308 inputs.len() <= expected,
309 "Verifier input count mismatch: expected at most {expected} public inputs, found {}",
310 inputs.len()
311 );
312 }
313 }
314 }
315
316 // Verify the execution proof.
317 Trace::verify_execution_proof(&locator, varuna_version, inclusion_version, verifier_inputs, execution)?;
318
319 lap!(timer, "Verify the proof");
320
321 finish!(timer);
322 Ok(())
323 }
324}
325
326impl<N: Network> Process<N> {
327 /// Returns the public inputs to verify the proof for the given transition.
328 fn to_transition_verifier_inputs(
329 transition: &Transition<N>,
330 parent: Option<&ProgramID<N>>,
331 call_graph: &HashMap<N::TransitionID, Vec<N::TransitionID>>,
332 program_checksum: Option<N::Field>,
333 transition_map: &HashMap<N::TransitionID, (&Transition<N>, Function<N>)>,
334 function_id_cache: &mut HashMap<(ProgramID<N>, Identifier<N>), Field<N>>,
335 network_id: &U16<N>,
336 execution_stacks: &IndexMap<ProgramID<N>, Arc<Stack<N>>>,
337 ) -> Result<Vec<N::Field>> {
338 // Compute the x- and y-coordinate of `tpk`.
339 let (tpk_x, tpk_y) = transition.tpk().to_xy_coordinates();
340
341 // Determine the value of `is_root` and `parent`.
342 let (is_root, parent) = match parent {
343 // If there is a parent, then `is_root` is `0` and `parent` is the parent program ID.
344 Some(program_id) => (Field::<N>::zero(), *program_id),
345 // If there is no parent, then `is_root` is `1` and `parent` is the root program ID.
346 None => (Field::one(), *transition.program_id()),
347 };
348
349 // Retrieve the address belonging to the parent.
350 let stack = execution_stacks.get(&parent).ok_or_else(|| anyhow!("Missing stack for program '{parent}'"))?;
351 let parent_address = stack.program_address();
352
353 // Compute the x- and y-coordinate of `parent`.
354 let (parent_x, parent_y) = parent_address.to_xy_coordinates();
355
356 // [Inputs] Construct the verifier inputs to verify the proof.
357 let mut verifier_inputs = vec![N::Field::one()];
358 // [Inputs] Extend the verifier inputs with the program checksum if it was provided.
359 if let Some(program_checksum) = program_checksum {
360 verifier_inputs.push(program_checksum);
361 }
362 // [Inputs] Extend the verifier inputs with the tpk, transition and signer commitments.
363 verifier_inputs.extend([*tpk_x, *tpk_y, **transition.tcm(), **transition.scm()]);
364 // [Inputs] Extend the verifier inputs with the input IDs.
365 verifier_inputs.extend(transition.inputs().iter().flat_map(|input| input.verifier_inputs()));
366 // [Inputs] Extend the verifier inputs with the public inputs for 'self.caller'.
367 verifier_inputs.extend([*is_root, *parent_x, *parent_y]);
368
369 // If there are function calls, append their inputs and outputs.
370 let child_transition_ids = call_graph.get(transition.id()).ok_or_else(|| {
371 anyhow!(
372 "Child transition IDs not found for transition {} (function: {})",
373 transition.id(),
374 transition.function_name()
375 )
376 })?;
377
378 // Retrieve the parent function from the transition map.
379 let parent_function = transition_map.get(transition.id()).map(|(_, function)| function.clone());
380
381 // Collect the function call instructions from the parent function.
382 // Each entry is (is_dynamic, instruction), where `is_dynamic` indicates a `call.dynamic`.
383 let parent_function_calls = match parent_function {
384 Some(function) => {
385 let mut calls = Vec::new();
386 for instruction in function.instructions() {
387 match instruction {
388 // Dynamic calls (`call.dynamic`) are always function calls and contribute to the call graph.
389 Instruction::CallDynamic(..) => calls.push(instruction.clone()),
390 // Static calls (`call`) are included only if they invoke a function (not a closure).
391 // Closures are inlined and do not produce separate transitions.
392 Instruction::Call(call) => {
393 // Retrieve the stack for the current program.
394 let stack = execution_stacks
395 .get(transition.program_id())
396 .ok_or_else(|| anyhow!("Missing stack for program '{}'", transition.program_id()))?;
397 // Check if this call invokes a function (as opposed to a closure).
398 match call.is_function_call(stack.as_ref()) {
399 Ok(true) => calls.push(instruction.clone()),
400 Ok(false) => { /* Closure call - skip */ }
401 Err(e) => bail!("Failed to determine if call is a function call: {e}"),
402 }
403 }
404 // All other instruction types (arithmetic, hashing, casting, etc.) are not function calls.
405 _ => {}
406 }
407 }
408 calls
409 }
410 // This should never occur, since `call_graph` and `transition_map` are populated together.
411 None => bail!("Function not found for transition {} ({})", transition.id(), transition.function_name()),
412 };
413
414 ensure!(
415 parent_function_calls.len() == child_transition_ids.len(),
416 "The number of parent function calls ({}) and child transition IDs ({}) do not match",
417 parent_function_calls.len(),
418 child_transition_ids.len()
419 );
420
421 for (child_transition_id, call_instruction) in child_transition_ids.iter().zip_eq(parent_function_calls) {
422 // Note: This unwrap is safe, as we are processing transitions in post-order,
423 // which implies that all child transition IDs have been added to `transition_map`.
424 let (child_transition, _) = transition_map.get(child_transition_id).unwrap();
425
426 // Retrieve (or compute and cache) the function ID for the child transition.
427 let child_function_id = get_or_compute_function_id(
428 function_id_cache,
429 network_id,
430 child_transition.program_id(),
431 child_transition.function_name(),
432 )?;
433
434 // Extract the `CallDynamic` instruction data, if this is a dynamic call.
435 let call_dynamic = match &call_instruction {
436 Instruction::CallDynamic(cd) => Some(cd),
437 Instruction::Call(..) => None,
438 // This should never occur, since `parent_function_calls` only contains `Call` and `CallDynamic`.
439 _ => bail!("Unexpected instruction type in parent function calls"),
440 };
441
442 // [Inputs] Extend the verifier inputs with the program ID and function name if the child transition is dynamic.
443 if call_dynamic.is_some() {
444 verifier_inputs.extend(child_transition.program_id().to_fields()?.into_iter().map(|field| *field));
445 verifier_inputs.extend([*child_transition.function_name().to_field()?]);
446 verifier_inputs.extend([*child_function_id]);
447 }
448 // [Inputs] Extend the verifier inputs with the transition commitment of the external call.
449 verifier_inputs.extend([**child_transition.tcm()]);
450
451 // [Inputs] Extend the verifier inputs with the input IDs of the external call.
452 let num_inputs = child_transition.inputs().len();
453 if let Some(call_dynamic_ref) = call_dynamic.as_ref() {
454 // For dynamic calls, use the dynamic ID when present, otherwise use normal verifier inputs.
455 let operand_types = call_dynamic_ref.operand_types();
456 ensure!(
457 child_transition.inputs().len() == operand_types.len(),
458 "The number of inputs ({}) and dynamic call operand types ({}) do not match",
459 child_transition.inputs().len(),
460 operand_types.len(),
461 );
462 for (i, (input, input_type)) in
463 child_transition.inputs().iter().zip_eq(operand_types.iter()).enumerate()
464 {
465 // Ensure the input is not a plain Record or ExternalRecord.
466 // Dynamic calls must use the `*WithDynamicID` variants for record inputs.
467 ensure!(
468 !matches!(input, Input::Record(..) | Input::ExternalRecord(..)),
469 "Input {i} in dynamic call to {} must not be a plain Record or ExternalRecord, found: {}",
470 child_transition.function_name(),
471 input,
472 );
473 // Ensure the input type matches the caller's expectation.
474 // Use the caller's view of the input (e.g., RecordWithDynamicID -> DynamicRecord).
475 ensure!(
476 input.to_caller_input().is_type(input_type),
477 "Input {i} in dynamic call to {} should be of type {}, found: {}",
478 child_transition.function_name(),
479 input_type,
480 input,
481 );
482 // Extend the verifier inputs based on the input variant.
483 match input {
484 // Inputs with a dynamic ID contribute only the dynamic ID.
485 Input::DynamicRecord(dynamic_id)
486 | Input::RecordWithDynamicID(_, _, dynamic_id)
487 | Input::ExternalRecordWithDynamicID(_, dynamic_id) => {
488 verifier_inputs.push(**dynamic_id);
489 }
490 // All other inputs contribute their standard verifier inputs.
491 Input::Constant(..) | Input::Public(..) | Input::Private(..) => {
492 verifier_inputs.extend(input.verifier_inputs());
493 }
494 // Record and ExternalRecord are excluded above.
495 Input::Record(..) | Input::ExternalRecord(..) => {
496 unreachable!("Record and ExternalRecord are excluded above")
497 }
498 }
499 }
500 } else {
501 verifier_inputs.extend(child_transition.inputs().iter().flat_map(|input| input.verifier_inputs()));
502 }
503
504 // [Outputs] Extend the verifier inputs with the output IDs of the external call.
505 if let Some(call_dynamic_ref) = call_dynamic.as_ref() {
506 let destination_types = call_dynamic_ref.destination_types();
507 ensure!(
508 child_transition.outputs().len() == destination_types.len(),
509 "The number of outputs ({}) and dynamic call destination types ({}) do not match",
510 child_transition.outputs().len(),
511 destination_types.len(),
512 );
513 for (index, (output, destination_type)) in
514 child_transition.outputs().iter().zip_eq(destination_types.iter()).enumerate()
515 {
516 match (output, destination_type) {
517 // A `Future` output with a `DynamicFuture` destination type: the verifier converts
518 // the static future to a dynamic future and computes its hash directly.
519 (Output::Future(_id, future), ValueType::DynamicFuture) => {
520 let Some(future) = future else {
521 bail!("Future is not present for child transition {}", child_transition.id());
522 };
523 let dynamic_future = DynamicFuture::from_future(future)?;
524 let output_index =
525 u16::try_from(num_inputs + index).map_err(|_| anyhow!("Output index exceeds u16"))?;
526 let output_id = OutputID::dynamic_future(
527 child_function_id,
528 &Value::DynamicFuture(dynamic_future),
529 *child_transition.tcm(),
530 output_index,
531 )?;
532 verifier_inputs.push(**output_id.id());
533 }
534 // Outputs with a dynamic ID contribute only the dynamic ID.
535 (Output::DynamicRecord(dynamic_id), _)
536 | (Output::RecordWithDynamicID(_, _, _, _, dynamic_id), _)
537 | (Output::ExternalRecordWithDynamicID(_, dynamic_id), _) => {
538 ensure!(
539 output.to_caller_output().is_type(destination_type),
540 "Output {index} in dynamic call to {} should be of type {}, found: {}",
541 child_transition.function_name(),
542 destination_type,
543 output,
544 );
545 verifier_inputs.push(**dynamic_id);
546 }
547 // All other outputs contribute their standard output ID.
548 (Output::Constant(..), _)
549 | (Output::Public(..), _)
550 | (Output::Private(..), _)
551 | (Output::Record(..), _)
552 | (Output::ExternalRecord(..), _)
553 | (Output::Future(..), _) => {
554 ensure!(
555 output.to_caller_output().is_type(destination_type),
556 "Output {index} in dynamic call to {} should be of type {}, found: {}",
557 child_transition.function_name(),
558 destination_type,
559 output,
560 );
561 verifier_inputs.push(**output.id());
562 }
563 }
564 }
565 } else {
566 verifier_inputs.extend(child_transition.output_ids().map(|id| **id));
567 }
568 }
569
570 // [Outputs] Extend the verifier inputs with the output IDs.
571 verifier_inputs.extend(transition.outputs().iter().flat_map(|output| output.verifier_inputs()));
572
573 dev_println!("Transition public inputs ({} elements): {:#?}", verifier_inputs.len(), verifier_inputs);
574 Ok(verifier_inputs)
575 }
576}
577
578/// Returns the function ID for the given program ID and function name, computing and caching it on demand.
579fn get_or_compute_function_id<N: Network>(
580 function_id_cache: &mut HashMap<(ProgramID<N>, Identifier<N>), Field<N>>,
581 network_id: &U16<N>,
582 program_id: &ProgramID<N>,
583 function_name: &Identifier<N>,
584) -> Result<Field<N>> {
585 let key = (*program_id, *function_name);
586 match function_id_cache.get(&key) {
587 Some(id) => Ok(*id),
588 None => {
589 let id = compute_function_id(network_id, program_id, function_name)?;
590 function_id_cache.insert(key, id);
591 Ok(id)
592 }
593 }
594}
595
596impl<N: Network> Process<N> {
597 // A helper function to construct a call graph from an execution.
598 //
599 // The call graph represents a mapping of parent transition IDs to child transition IDs,
600 // in the order that they were called.
601 //
602 // Suppose we have the following call structure.
603 // The functions are invoked in the following order:
604 // "three.aleo/a"
605 // --> "two.aleo/b"
606 // --> "zero.aleo/c"
607 // --> "zero.aleo/c"
608 // --> "one.aleo/d"
609 // --> "zero.aleo/c"
610 // The order of the transitions in the `Execution` is:
611 // - [c, b, c, c, d, a]
612 // However, the `Execution` only provides `Transition`s and not the call graph.
613 // In other words, we do not know which transitions were invoked by which transitions.
614 // Note that transition names are insufficient to reconstruct the call graph, since the same function can be invoked multiple times, in different ways.
615 //
616 // In order to reconstruct the call graph, we:
617 // - Iterate over the call structure in reverse post-order. The ordering is maintained by the `traversal_stack`.
618 // - Process each transition in the `Execution` in reverse, assigning its transition ID to the corresponding function call.
619 pub fn construct_call_graph<'a>(
620 transitions: impl ExactSizeIterator<Item = &'a Transition<N>> + DoubleEndedIterator,
621 execution_stacks: &IndexMap<ProgramID<N>, Arc<Stack<N>>>,
622 ) -> Result<HashMap<N::TransitionID, Vec<N::TransitionID>>> {
623 // Metadata for each transition the execution.
624 struct TransitionMetadata<N: Network> {
625 uid: usize,
626 // pid and fname of the transition. For static calls, this is set at
627 // metadata-creation time to be later matched against the data from
628 // the actual transition found in the execution (defense in depth).
629 // For dynamic calls, it is set to None and subsequently taken from
630 // the data in the actual transition (no in-depth defense).
631 locator: Option<(ProgramID<N>, Identifier<N>)>,
632 tid: Option<N::TransitionID>,
633 children: Option<Vec<usize>>,
634 }
635
636 impl<N: Network> TransitionMetadata<N> {
637 fn new(
638 counter: &mut usize,
639 locator: Option<(ProgramID<N>, Identifier<N>)>,
640 tid: Option<N::TransitionID>,
641 ) -> Self {
642 let uid = *counter;
643 *counter += 1;
644 Self { uid, locator, tid, children: None }
645 }
646
647 /// Returns 'true' if the subgraph starting from this transition has been fully-indexed.
648 fn is_complete(&self) -> bool {
649 self.tid.is_some() && self.children.is_some()
650 }
651 }
652
653 // A helper function to update the call graph, given transition metadata.
654 let update_call_graph = |metadata: TransitionMetadata<N>,
655 call_graph: &mut HashMap<N::TransitionID, Vec<N::TransitionID>>,
656 uid_to_tid: &mut HashMap<usize, N::TransitionID>|
657 -> Result<()> {
658 // Check that the transition metadata is complete.
659 ensure!(metadata.is_complete(), "Invalid traversal - transition metadata is incomplete");
660 // Update the call graph.
661 call_graph.insert(
662 metadata.tid.unwrap(),
663 metadata
664 .children // Safe to unwrap, since the metadata is complete.
665 .unwrap()
666 .into_iter()
667 .map(|uid| match uid_to_tid.get(&uid) {
668 Some(tid) => Ok(*tid),
669 None => bail!("Invalid traversal - missing 'tid' for uid '{uid}'"),
670 })
671 .collect::<Result<Vec<_>, _>>()?,
672 );
673 // Update the UID to TID mapping.
674 uid_to_tid.insert(metadata.uid, metadata.tid.unwrap());
675 Ok(())
676 };
677
678 // Initialize a call graph, which is a map of transition IDs to the transition IDs it calls.
679 let mut call_graph = HashMap::new();
680 // Initialize a mapping from UIDs to transition IDs.
681 let mut uid_to_tid = HashMap::new();
682
683 // Initialize a stack to track transition metadata, while traversing the call graph.
684 let mut traversal_stack: Vec<TransitionMetadata<N>> = Vec::new();
685 // Initialize a counter to provide unique IDs for each transition.
686 let mut counter = 0;
687
688 let num_transitions = transitions.len();
689
690 // Iterate over each transition in reverse post-order, and populate the call graph.
691 for transition in transitions.rev() {
692 // Now process the current `transition`.
693 // At this point, the algorithm must maintain the following invariant:
694 // - The stack is either empty, or the top entry is incomplete.
695 match traversal_stack.last_mut() {
696 // If the stack is empty, then push the `transition` to the top of the stack.
697 None => {
698 ensure!(
699 counter == 0,
700 "Invalid traversal - execution contains multiple disconnected transition trees"
701 );
702 traversal_stack.push(TransitionMetadata::new(
703 &mut counter,
704 Some((*transition.program_id(), *transition.function_name())),
705 Some(*transition.id()),
706 ));
707 }
708 // If the stack is not empty, then add the current transition ID to the entry.
709 Some(head) => {
710 match head.locator {
711 Some((expected_pid, expected_fname)) => {
712 // Checking the pid and fname expected (from the static call instruction) against the actual transition.
713 ensure!(
714 expected_pid == *transition.program_id()
715 && expected_fname == *transition.function_name(),
716 "Invalid traversal - unexpected transition in the execution"
717 );
718 }
719 None => {
720 // Setting the pid and fname from the actual transition
721 head.locator = Some((*transition.program_id(), *transition.function_name()));
722 }
723 }
724
725 head.tid = Some(*transition.id());
726 }
727 }
728
729 // Process the entry at the top of the stack. By the previous step, this entry has a transition ID.
730 // Note this unwrap is safe, since we either pushed an entry to the stack or modified the one at the top of the stack.
731 let top = traversal_stack.last().unwrap();
732 // If the entry is complete, then add it to the call graph.
733 if top.is_complete() {
734 // Note this unwrap is safe, for the same reason as above.
735 update_call_graph(traversal_stack.pop().unwrap(), &mut call_graph, &mut uid_to_tid)?;
736 } else {
737 // This unwrap is safe as the locator field is set after all possible paths of the match
738 let (caller_pid, caller_fname) = top.locator.as_ref().unwrap();
739
740 // Retrieve the stack.
741 let stack = execution_stacks
742 .get(caller_pid)
743 .ok_or_else(|| anyhow!("Missing stack for program '{caller_pid}'"))?;
744 // Retrieve the function from the stack.
745 let caller_fname = stack.get_function(caller_fname)?;
746 // Collect the children of the current transition.
747 let mut children = Vec::new();
748 for instruction in caller_fname.instructions() {
749 if let Instruction::Call(call) = instruction {
750 let (pid, fname) = match call.operator() {
751 snarkvm_synthesizer_program::CallOperator::Locator(locator) => {
752 (locator.program_id(), locator.resource())
753 }
754 snarkvm_synthesizer_program::CallOperator::Resource(fname) => (caller_pid, fname),
755 };
756 // Add the child to the traversal stack, only if it is a call to a transition.
757 if execution_stacks.get(pid).is_some_and(|stack| stack.get_function(fname).is_ok()) {
758 children.push(TransitionMetadata::new(&mut counter, Some((*pid, *fname)), None));
759 }
760 }
761 if let Instruction::CallDynamic(_) = instruction {
762 // Add the child to the traversal stack.
763 // NOTE: for dynamic calls, the verifier doesn't have
764 // access to a locator or resource. However, the
765 // verifier can determine the program and function name
766 // directly from the DFS ordering of transitions in the
767 // Execution.
768 children.push(TransitionMetadata::new(&mut counter, None, None));
769 }
770 }
771
772 // Add the children UIDs to the metadata.
773 // Note this unwrap is safe, for the same reason as above.
774 let top = traversal_stack.last_mut().unwrap();
775 let child_uids = children.iter().map(|child| child.uid).collect::<Vec<_>>();
776 match top.children {
777 None => top.children = Some(child_uids),
778 Some(_) => bail!("Invalid traversal - children have already been processed"),
779 }
780 // Push the children to the top of the stack.
781 traversal_stack.extend(children);
782 }
783 // If the stack has complete metadata entries, then remove and add them to the call graph.
784 while let Some(metadata) = traversal_stack.last() {
785 if metadata.is_complete() {
786 update_call_graph(traversal_stack.pop().unwrap(), &mut call_graph, &mut uid_to_tid)?;
787 } else {
788 break;
789 }
790 }
791 }
792 // Check that the traversal completed correctly.
793 ensure!(traversal_stack.is_empty(), "Invalid traversal - traversal stack is not empty");
794
795 ensure!(
796 counter == num_transitions,
797 "Invalid traversal - counter does not match the number of transitions in the execution"
798 );
799
800 Ok(call_graph)
801 }
802
803 /// A helper function to reverse the call graph.
804 ///
805 /// The call graph is a mapping of parent transition IDs to child transition IDs,
806 /// in the order that they were called.
807 ///
808 /// The reverse call graph is a mapping of child transition IDs to parent transition IDs.
809 /// Note: Each child transition only has one parent transition, by definition.
810 pub(crate) fn reverse_call_graph(
811 call_graph: &HashMap<N::TransitionID, Vec<N::TransitionID>>,
812 ) -> HashMap<N::TransitionID, N::TransitionID> {
813 // Initialize a map for the reverse call graph.
814 let mut reverse_call_graph = HashMap::new();
815 // Iterate over the (forward) call graph.
816 for (parent, children) in call_graph {
817 for child in children {
818 let result = reverse_call_graph.insert(*child, *parent);
819 debug_assert!(result.is_none(), "Found a child with multiple parents");
820 }
821 }
822 reverse_call_graph
823 }
824}