Skip to main content

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}