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}