Skip to main content

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