Skip to main content

snarkvm_ledger_block/transition/
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
16pub mod input;
17pub use input::Input;
18
19pub mod output;
20pub use output::Output;
21
22mod bytes;
23mod merkle;
24mod serialize;
25mod string;
26
27use console::{
28    network::prelude::*,
29    program::{
30        Ciphertext,
31        Identifier,
32        InputID,
33        OutputID,
34        ProgramID,
35        Record,
36        Register,
37        Request,
38        Response,
39        TRANSITION_DEPTH,
40        ToFields,
41        TransitionLeaf,
42        TransitionPath,
43        TransitionTree,
44        Value,
45        ValueType,
46        compute_function_id,
47    },
48    types::{Address, Field, Group},
49};
50
51/// Computes an output hash as `Hash(function_id || value_fields || tvk || index)`.
52fn compute_output_hash<N: Network>(
53    function_id: Field<N>,
54    value: &impl ToFields<Field = Field<N>>,
55    tvk: &Field<N>,
56    num_inputs: usize,
57    index: usize,
58) -> Result<Field<N>> {
59    let index = Field::from_u16(u16::try_from(num_inputs + index)?);
60    let mut preimage = vec![function_id];
61    preimage.extend(value.to_fields()?);
62    preimage.push(*tvk);
63    preimage.push(index);
64    N::hash_psd8(&preimage)
65}
66
67#[derive(Clone, PartialEq, Eq)]
68pub struct Transition<N: Network> {
69    /// The transition ID.
70    id: N::TransitionID,
71    /// The program ID.
72    program_id: ProgramID<N>,
73    /// The function name.
74    function_name: Identifier<N>,
75    /// The transition inputs.
76    inputs: Vec<Input<N>>,
77    /// The transition outputs.
78    outputs: Vec<Output<N>>,
79    /// The transition public key.
80    tpk: Group<N>,
81    /// The transition commitment.
82    tcm: Field<N>,
83    /// The transition signer commitment.
84    scm: Field<N>,
85}
86
87impl<N: Network> Transition<N> {
88    /// Initializes a new transition.
89    #[allow(clippy::too_many_arguments)]
90    pub fn new(
91        program_id: ProgramID<N>,
92        function_name: Identifier<N>,
93        inputs: Vec<Input<N>>,
94        outputs: Vec<Output<N>>,
95        tpk: Group<N>,
96        tcm: Field<N>,
97        scm: Field<N>,
98    ) -> Result<Self> {
99        // Compute the function tree.
100        let function_tree = Self::function_tree(&inputs, &outputs)?;
101        // Compute the transition ID as `hash(root, tcm)`.
102        let id = N::hash_bhp512(&(*function_tree.root(), tcm).to_bits_le())?.into();
103        // Return the transition.
104        Ok(Self { id, program_id, function_name, inputs, outputs, tpk, tcm, scm })
105    }
106
107    /// Initializes a new transition from a request, response, and optional dynamic outputs.
108    pub fn from(
109        request: &Request<N>,
110        response: &Response<N>,
111        output_types: &[ValueType<N>],
112        output_registers: &[Option<Register<N>>],
113    ) -> Result<Self> {
114        let network_id = *request.network_id();
115        let program_id = *request.program_id();
116        let function_name = *request.function_name();
117        let num_inputs = request.inputs().len();
118
119        // Compute the function ID based on the whether the request and response are dynamic.
120        let function_id = compute_function_id(&network_id, &program_id, &function_name)?;
121
122        // A helper function to construct and verify the inputs.
123        // If caller_input_ids is provided (for dynamic calls), it's used to determine if
124        // the caller sees a dynamic record while the callee sees a static one.
125        let construct_inputs = |input_ids: &[InputID<N>],
126                                inputs: &[Value<N>],
127                                caller_input_ids: Option<&[InputID<N>]>|
128         -> Result<Vec<Input<N>>> {
129            ensure!(
130                input_ids.len() == inputs.len(),
131                "Mismatched number of input IDs and inputs: {} vs. {}",
132                input_ids.len(),
133                inputs.len(),
134            );
135            if let Some(caller_ids) = caller_input_ids {
136                ensure!(
137                    caller_ids.len() == inputs.len(),
138                    "Mismatched number of caller input IDs and inputs: {} vs. {}",
139                    caller_ids.len(),
140                    inputs.len(),
141                );
142            }
143
144            input_ids
145                .iter()
146                .zip_eq(inputs)
147                .enumerate()
148                .map(|(index, (input_id, input))| {
149                    // Get the caller's input ID for this index (if available).
150                    let caller_input_id = caller_input_ids.map(|ids| &ids[index]);
151
152                    // Construct the transition input.
153                    match (input_id, input) {
154                        (InputID::Constant(input_hash), Value::Plaintext(plaintext)) => {
155                            // Construct the constant input.
156                            let input = Input::Constant(*input_hash, Some(plaintext.clone()));
157                            // Ensure the input is valid.
158                            match input.verify(function_id, request.tcm(), index) {
159                                true => Ok(input),
160                                false => bail!("Malformed constant transition input: '{input}'"),
161                            }
162                        }
163                        (InputID::Public(input_hash), Value::Plaintext(plaintext)) => {
164                            // Construct the public input.
165                            let input = Input::Public(*input_hash, Some(plaintext.clone()));
166                            // Ensure the input is valid.
167                            match input.verify(function_id, request.tcm(), index) {
168                                true => Ok(input),
169                                false => bail!("Malformed public transition input: '{input}'"),
170                            }
171                        }
172                        (InputID::Private(input_hash), Value::Plaintext(plaintext)) => {
173                            // Construct the (console) input index as a field element.
174                            let index = Field::from_u16(index as u16);
175                            // Compute the ciphertext, with the input view key as `Hash(function ID || tvk || index)`.
176                            let ciphertext =
177                                plaintext.encrypt_symmetric(N::hash_psd4(&[function_id, *request.tvk(), index])?)?;
178                            // Compute the ciphertext hash.
179                            let ciphertext_hash = N::hash_psd8(&ciphertext.to_fields()?)?;
180                            // Ensure the ciphertext hash matches.
181                            ensure!(*input_hash == ciphertext_hash, "The input ciphertext hash is incorrect");
182                            // Return the private input.
183                            Ok(Input::Private(*input_hash, Some(ciphertext)))
184                        }
185                        (InputID::Record(_, _, _, serial_number, tag), Value::Record(..)) => {
186                            // Check if caller sees this as a dynamic record.
187                            if let Some(InputID::DynamicRecord(dynamic_id)) = caller_input_id {
188                                // Return the record with dynamic ID.
189                                Ok(Input::RecordWithDynamicID(*serial_number, *tag, *dynamic_id))
190                            } else {
191                                // Return the input record.
192                                Ok(Input::Record(*serial_number, *tag))
193                            }
194                        }
195                        (InputID::ExternalRecord(input_hash), Value::Record(..)) => {
196                            // Check if caller sees this as a dynamic record.
197                            if let Some(InputID::DynamicRecord(dynamic_id)) = caller_input_id {
198                                // Return the external record with dynamic ID.
199                                Ok(Input::ExternalRecordWithDynamicID(*input_hash, *dynamic_id))
200                            } else {
201                                // Return the input external record.
202                                Ok(Input::ExternalRecord(*input_hash))
203                            }
204                        }
205                        (InputID::DynamicRecord(input_hash), Value::DynamicRecord(..)) => {
206                            // Return the input dynamic record.
207                            Ok(Input::DynamicRecord(*input_hash))
208                        }
209                        _ => bail!("Malformed request input: {input_id:?}, {input}"),
210                    }
211                })
212                .collect::<Result<Vec<_>>>()
213        };
214
215        // Get caller context upfront if the request is dynamic.
216        let (caller_input_ids, caller_output_values) = if request.is_dynamic() {
217            (Some(request.to_dynamic_input_ids()?), Some(response.to_dynamic_outputs()?))
218        } else {
219            (None, None)
220        };
221
222        // Construct and verify the inputs (with caller context for dynamic calls).
223        let inputs = construct_inputs(request.input_ids(), request.inputs(), caller_input_ids.as_deref())?;
224
225        // Construct and verify the outputs.
226        {
227            let num_outputs = response.outputs().len();
228
229            ensure!(
230                response.output_ids().len() == num_outputs
231                    && num_outputs == output_types.len()
232                    && num_outputs == output_registers.len(),
233                "Mismatched number of output IDs, outputs, output types, and output registers: {} vs. {} vs. {} vs. {}",
234                response.output_ids().len(),
235                num_outputs,
236                output_types.len(),
237                output_registers.len(),
238            );
239
240            // Verify caller output values length if provided.
241            if let Some(ref caller_values) = caller_output_values {
242                ensure!(
243                    caller_values.len() == num_outputs,
244                    "Mismatched caller outputs and callee outputs: {} vs. {}",
245                    caller_values.len(),
246                    num_outputs
247                );
248            }
249        }
250
251        // Construct outputs with caller context for dynamic calls.
252        let outputs = itertools::izip!(response.output_ids(), response.outputs(), output_types, output_registers)
253            .enumerate()
254            .map(|(output_index, (output_id, output, output_type, output_register))| {
255                // Get the caller's value for this output (if available).
256                let caller_value = caller_output_values.as_ref().map(|values| &values[output_index]);
257                Self::construct_output(
258                    function_id,
259                    &program_id,
260                    num_inputs,
261                    request.tvk(),
262                    request.tcm(),
263                    request.signer(),
264                    output_index,
265                    &Some(output_id.clone()),
266                    output,
267                    output_type,
268                    output_register,
269                    caller_value,
270                )
271            })
272            .collect::<Result<Vec<_>>>()?;
273
274        // Retrieve the `tpk`.
275        let tpk = request.to_tpk();
276        // Retrieve the `tcm`.
277        let tcm = *request.tcm();
278        // Retrieve the `scm`.
279        let scm = *request.scm();
280        // Return the transition.
281        Self::new(program_id, function_name, inputs, outputs, tpk, tcm, scm)
282    }
283
284    /// Initializes a new transition from a request, response, and optional
285    /// dynamic outputs. It does not check correctness or consistency of the
286    /// provided values and should only be used for cost-estimation or testing
287    /// purposes.
288    pub fn from_unchecked(
289        request: &Request<N>,
290        response: &Response<N>,
291        output_types: &[ValueType<N>],
292        output_registers: &[Option<Register<N>>],
293    ) -> Result<Self> {
294        let network_id = *request.network_id();
295        let program_id = *request.program_id();
296        let function_name = *request.function_name();
297        let num_inputs = request.inputs().len();
298
299        // Compute the function ID based on the whether the request and response are dynamic.
300        let function_id = compute_function_id(&network_id, &program_id, &function_name)?;
301
302        // A helper function to construct inputs without verifying any of their fields.
303        // If caller_input_ids is provided (for dynamic calls), it's used to determine if
304        // the caller sees a dynamic record while the callee sees a static one.
305        let construct_inputs = |input_ids: &[InputID<N>],
306                                inputs: &[Value<N>],
307                                caller_input_ids: Option<&[InputID<N>]>|
308         -> Result<Vec<Input<N>>> {
309            ensure!(
310                input_ids.len() == inputs.len(),
311                "Mismatched number of input IDs and inputs: {} vs. {}",
312                input_ids.len(),
313                inputs.len(),
314            );
315            if let Some(caller_ids) = caller_input_ids {
316                ensure!(
317                    caller_ids.len() == inputs.len(),
318                    "Mismatched number of caller input IDs and inputs: {} vs. {}",
319                    caller_ids.len(),
320                    inputs.len(),
321                );
322            }
323
324            input_ids
325                .iter()
326                .zip_eq(inputs)
327                .enumerate()
328                .map(|(index, (input_id, input))| {
329                    // Get the caller's input ID for this index (if available).
330                    let caller_input_id = caller_input_ids.map(|ids| &ids[index]);
331
332                    // Construct the transition input.
333                    match (input_id, input) {
334                        (InputID::Constant(input_hash), Value::Plaintext(plaintext)) => {
335                            // Construct the constant input.
336                            Ok(Input::Constant(*input_hash, Some(plaintext.clone())))
337                        }
338                        (InputID::Public(input_hash), Value::Plaintext(plaintext)) => {
339                            // Construct the public input.
340                            Ok(Input::Public(*input_hash, Some(plaintext.clone())))
341                        }
342                        (InputID::Private(input_hash), Value::Plaintext(plaintext)) => {
343                            // Construct the (console) input index as a field element.
344                            let index = Field::from_u16(index as u16);
345                            // Compute the ciphertext, with the input view key as `Hash(function ID || tvk || index)`.
346                            let ciphertext =
347                                plaintext.encrypt_symmetric(N::hash_psd4(&[function_id, *request.tvk(), index])?)?;
348                            // Return the private input.
349                            Ok(Input::Private(*input_hash, Some(ciphertext)))
350                        }
351                        (InputID::Record(_, _, _, serial_number, tag), Value::Record(..)) => {
352                            // Check if caller sees this as a dynamic record.
353                            if let Some(InputID::DynamicRecord(dynamic_id)) = caller_input_id {
354                                // Return the record with dynamic ID.
355                                Ok(Input::RecordWithDynamicID(*serial_number, *tag, *dynamic_id))
356                            } else {
357                                // Return the input record.
358                                Ok(Input::Record(*serial_number, *tag))
359                            }
360                        }
361                        (InputID::ExternalRecord(input_hash), Value::Record(..)) => {
362                            // Check if caller sees this as a dynamic record.
363                            if let Some(InputID::DynamicRecord(dynamic_id)) = caller_input_id {
364                                // Return the external record with dynamic ID.
365                                Ok(Input::ExternalRecordWithDynamicID(*input_hash, *dynamic_id))
366                            } else {
367                                // Return the input external record.
368                                Ok(Input::ExternalRecord(*input_hash))
369                            }
370                        }
371                        (InputID::DynamicRecord(input_hash), Value::DynamicRecord(..)) => {
372                            // Return the input dynamic record.
373                            Ok(Input::DynamicRecord(*input_hash))
374                        }
375                        _ => bail!("Malformed request input: {input_id:?}, {input}"),
376                    }
377                })
378                .collect::<Result<Vec<_>>>()
379        };
380
381        // Get caller context upfront if the request is dynamic.
382        let (caller_input_ids, caller_output_values) = if request.is_dynamic() {
383            (Some(request.to_dynamic_input_ids()?), Some(response.to_dynamic_outputs()?))
384        } else {
385            (None, None)
386        };
387
388        // Construct and verify the inputs (with caller context for dynamic calls).
389        let inputs = construct_inputs(request.input_ids(), request.inputs(), caller_input_ids.as_deref())?;
390
391        // Construct and verify the outputs.
392        {
393            let num_outputs = response.outputs().len();
394
395            ensure!(
396                response.output_ids().len() == num_outputs
397                    && num_outputs == output_types.len()
398                    && num_outputs == output_registers.len(),
399                "Mismatched number of output IDs, outputs, output types, and output registers: {} vs. {} vs. {} vs. {}",
400                response.output_ids().len(),
401                num_outputs,
402                output_types.len(),
403                output_registers.len(),
404            );
405
406            // Verify caller output values length if provided.
407            if let Some(ref caller_values) = caller_output_values {
408                ensure!(
409                    caller_values.len() == num_outputs,
410                    "Mismatched caller outputs and callee outputs: {} vs. {}",
411                    caller_values.len(),
412                    num_outputs
413                );
414            }
415        }
416
417        // Construct outputs with caller context for dynamic calls.
418        let outputs = itertools::izip!(response.output_ids(), response.outputs(), output_types, output_registers)
419            .enumerate()
420            .map(|(output_index, (output_id, output, output_type, output_register))| {
421                // Get the caller's value for this output (if available).
422                let caller_value = caller_output_values.as_ref().map(|values| &values[output_index]);
423                Self::construct_output(
424                    function_id,
425                    &program_id,
426                    num_inputs,
427                    request.tvk(),
428                    request.tcm(),
429                    request.signer(),
430                    output_index,
431                    &Some(output_id.clone()),
432                    output,
433                    output_type,
434                    output_register,
435                    caller_value,
436                )
437            })
438            .collect::<Result<Vec<_>>>()?;
439
440        // Retrieve the `tpk`.
441        let tpk = request.to_tpk();
442        // Retrieve the `tcm`.
443        let tcm = *request.tcm();
444        // Retrieve the `scm`.
445        let scm = *request.scm();
446        // Return the transition.
447        Self::new(program_id, function_name, inputs, outputs, tpk, tcm, scm)
448    }
449}
450
451impl<N: Network> Transition<N> {
452    /// Returns the transition ID.
453    pub const fn id(&self) -> &N::TransitionID {
454        &self.id
455    }
456
457    /// Returns the program ID.
458    pub const fn program_id(&self) -> &ProgramID<N> {
459        &self.program_id
460    }
461
462    /// Returns the function name.
463    pub const fn function_name(&self) -> &Identifier<N> {
464        &self.function_name
465    }
466
467    /// Returns the inputs.
468    pub fn inputs(&self) -> &[Input<N>] {
469        &self.inputs
470    }
471
472    /// Return the outputs.
473    pub fn outputs(&self) -> &[Output<N>] {
474        &self.outputs
475    }
476
477    /// Returns the transition public key.
478    pub const fn tpk(&self) -> &Group<N> {
479        &self.tpk
480    }
481
482    /// Returns the transition commitment.
483    pub const fn tcm(&self) -> &Field<N> {
484        &self.tcm
485    }
486
487    /// Returns the signer commitment.
488    pub const fn scm(&self) -> &Field<N> {
489        &self.scm
490    }
491}
492
493impl<N: Network> Transition<N> {
494    /// Returns `true` if this is a `credits.aleo/*` transition.
495    #[inline]
496    pub fn is_credits(&self) -> bool {
497        self.program_id.to_string() == "credits.aleo"
498    }
499
500    /// Returns `true` if this is a `bond_public` transition.
501    #[inline]
502    pub fn is_bond_public(&self) -> bool {
503        self.inputs.len() == 3
504            && self.outputs.len() == 1
505            && self.program_id.to_string() == "credits.aleo"
506            && self.function_name.to_string() == "bond_public"
507    }
508
509    /// Returns `true` if this is a `bond_validator` transition.
510    #[inline]
511    pub fn is_bond_validator(&self) -> bool {
512        self.inputs.len() == 3
513            && self.outputs.len() == 1
514            && self.program_id.to_string() == "credits.aleo"
515            && self.function_name.to_string() == "bond_validator"
516    }
517
518    /// Returns `true` if this is an `unbond_public` transition.
519    #[inline]
520    pub fn is_unbond_public(&self) -> bool {
521        self.inputs.len() == 2
522            && self.outputs.len() == 1
523            && self.program_id.to_string() == "credits.aleo"
524            && self.function_name.to_string() == "unbond_public"
525    }
526
527    /// Returns `true` if this is a `fee_private` transition.
528    #[inline]
529    pub fn is_fee_private(&self) -> bool {
530        self.inputs.len() == 4
531            && self.outputs.len() == 1
532            && self.program_id.to_string() == "credits.aleo"
533            && self.function_name.to_string() == "fee_private"
534    }
535
536    /// Returns `true` if this is a `fee_public` transition.
537    #[inline]
538    pub fn is_fee_public(&self) -> bool {
539        self.inputs.len() == 3
540            && self.outputs.len() == 1
541            && self.program_id.to_string() == "credits.aleo"
542            && self.function_name.to_string() == "fee_public"
543    }
544
545    /// Returns `true` if this is a `split` transition.
546    #[inline]
547    pub fn is_split(&self) -> bool {
548        self.inputs.len() == 2
549            && self.outputs.len() == 2
550            && self.program_id.to_string() == "credits.aleo"
551            && self.function_name.to_string() == "split"
552    }
553
554    /// Returns `true` if this is an `upgrade` transition.
555    #[inline]
556    pub fn is_upgrade(&self) -> bool {
557        self.inputs.len() == 1
558            && self.outputs.len() == 2
559            && self.program_id.to_string() == "credits.aleo"
560            && self.function_name.to_string() == "upgrade"
561    }
562}
563
564impl<N: Network> Transition<N> {
565    /// Returns `true` if the transition contains the given serial number.
566    pub fn contains_serial_number(&self, serial_number: &Field<N>) -> bool {
567        self.inputs.iter().any(|input| match input {
568            Input::Constant(_, _) => false,
569            Input::Public(_, _) => false,
570            Input::Private(_, _) => false,
571            Input::Record(input_sn, _) | Input::RecordWithDynamicID(input_sn, _, _) => input_sn == serial_number,
572            Input::ExternalRecord(_) => false,
573            Input::DynamicRecord(_) => false,
574            Input::ExternalRecordWithDynamicID(_, _) => false,
575        })
576    }
577
578    /// Returns `true` if the transition contains the given commitment.
579    pub fn contains_commitment(&self, commitment: &Field<N>) -> bool {
580        self.outputs.iter().any(|output| match output {
581            Output::Constant(_, _) => false,
582            Output::Public(_, _) => false,
583            Output::Private(_, _) => false,
584            Output::Record(output_cm, _, _, _) | Output::RecordWithDynamicID(output_cm, _, _, _, _) => {
585                output_cm == commitment
586            }
587            Output::ExternalRecord(_) => false,
588            Output::Future(_, _) => false,
589            Output::DynamicRecord(_) => false,
590            Output::ExternalRecordWithDynamicID(_, _) => false,
591        })
592    }
593}
594
595impl<N: Network> Transition<N> {
596    /// Returns the record with the corresponding commitment, if it exists.
597    pub fn find_record(&self, commitment: &Field<N>) -> Option<&Record<N, Ciphertext<N>>> {
598        self.outputs.iter().find_map(|output| match output {
599            Output::Constant(_, _) => None,
600            Output::Public(_, _) => None,
601            Output::Private(_, _) => None,
602            Output::Record(output_cm, _, Some(record), _) if output_cm == commitment => Some(record),
603            Output::Record(_, _, _, _) => None,
604            Output::RecordWithDynamicID(output_cm, _, Some(record), _, _) if output_cm == commitment => Some(record),
605            Output::RecordWithDynamicID(_, _, _, _, _) => None,
606            Output::ExternalRecord(_) => None,
607            Output::Future(_, _) => None,
608            Output::DynamicRecord(_) => None,
609            Output::ExternalRecordWithDynamicID(_, _) => None,
610        })
611    }
612}
613
614impl<N: Network> Transition<N> {
615    /* Input */
616
617    /// Returns the input IDs.
618    pub fn input_ids(&self) -> impl '_ + ExactSizeIterator<Item = &Field<N>> {
619        self.inputs.iter().map(Input::id)
620    }
621
622    /// Returns an iterator over the serial numbers, for inputs that are records.
623    pub fn serial_numbers(&self) -> impl '_ + Iterator<Item = &Field<N>> {
624        self.inputs.iter().flat_map(Input::serial_number)
625    }
626
627    /// Returns an iterator over the tags, for inputs that are records.
628    pub fn tags(&self) -> impl '_ + Iterator<Item = &Field<N>> {
629        self.inputs.iter().flat_map(Input::tag)
630    }
631
632    /* Output */
633
634    /// Returns the output IDs.
635    pub fn output_ids(&self) -> impl '_ + ExactSizeIterator<Item = &Field<N>> {
636        self.outputs.iter().map(Output::id)
637    }
638
639    /// Returns an iterator over the commitments, for outputs that are records.
640    pub fn commitments(&self) -> impl '_ + Iterator<Item = &Field<N>> {
641        self.outputs.iter().flat_map(Output::commitment)
642    }
643
644    /// Returns an iterator over the nonces, for outputs that are records.
645    pub fn nonces(&self) -> impl '_ + Iterator<Item = &Group<N>> {
646        self.outputs.iter().flat_map(Output::nonce)
647    }
648
649    /// Returns an iterator over the output records, as a tuple of `(commitment, record)`.
650    pub fn records(&self) -> impl '_ + Iterator<Item = (&Field<N>, &Record<N, Ciphertext<N>>)> {
651        self.outputs.iter().flat_map(Output::record)
652    }
653}
654
655impl<N: Network> Transition<N> {
656    /// Returns the transition ID, and consumes `self`.
657    pub fn into_id(self) -> N::TransitionID {
658        self.id
659    }
660
661    /* Input */
662
663    /// Returns a consuming iterator over the serial numbers, for inputs that are records.
664    pub fn into_serial_numbers(self) -> impl Iterator<Item = Field<N>> {
665        self.inputs.into_iter().flat_map(Input::into_serial_number)
666    }
667
668    /// Returns a consuming iterator over the tags, for inputs that are records.
669    pub fn into_tags(self) -> impl Iterator<Item = Field<N>> {
670        self.inputs.into_iter().flat_map(Input::into_tag)
671    }
672
673    /* Output */
674
675    /// Returns a consuming iterator over the commitments, for outputs that are records.
676    pub fn into_commitments(self) -> impl Iterator<Item = Field<N>> {
677        self.outputs.into_iter().flat_map(Output::into_commitment)
678    }
679
680    /// Returns a consuming iterator over the nonces, for outputs that are records.
681    pub fn into_nonces(self) -> impl Iterator<Item = Group<N>> {
682        self.outputs.into_iter().flat_map(Output::into_nonce)
683    }
684
685    /// Returns a consuming iterator over the output records, as a tuple of `(commitment, record)`.
686    pub fn into_records(self) -> impl Iterator<Item = (Field<N>, Record<N, Ciphertext<N>>)> {
687        self.outputs.into_iter().flat_map(Output::into_record)
688    }
689
690    /// Returns the transition public key, and consumes `self`.
691    pub fn into_tpk(self) -> Group<N> {
692        self.tpk
693    }
694}
695
696// A helper function to construct and verify the outputs. If caller_value is
697// provided (for dynamic calls), it's used to determine if the caller sees a
698// dynamic record while the callee sees a static one.
699impl<N: Network> Transition<N> {
700    fn construct_output(
701        function_id: Field<N>,
702        program_id: &ProgramID<N>,
703        num_inputs: usize,
704        tvk: &Field<N>,
705        tcm: &Field<N>,
706        signer: &Address<N>,
707        index: usize,
708        output_id: &Option<OutputID<N>>,
709        output: &Value<N>,
710        output_type: &ValueType<N>,
711        output_register: &Option<Register<N>>,
712        caller_value: Option<&Value<N>>,
713    ) -> Result<Output<N>> {
714        // Construct the transition output.
715        match (output_id, output) {
716            (Some(OutputID::Constant(output_hash)), Value::Plaintext(plaintext)) => {
717                // Construct the constant output.
718                let output = Output::Constant(*output_hash, Some(plaintext.clone()));
719                // Ensure the output is valid.
720                match output.verify(function_id, tcm, num_inputs + index) {
721                    true => Ok(output),
722                    false => bail!("Malformed constant transition output: '{output}'"),
723                }
724            }
725            (Some(OutputID::Public(output_hash)), Value::Plaintext(plaintext)) => {
726                // Construct the public output.
727                let output = Output::Public(*output_hash, Some(plaintext.clone()));
728                // Ensure the output is valid.
729                match output.verify(function_id, tcm, num_inputs + index) {
730                    true => Ok(output),
731                    false => bail!("Malformed public transition output: '{output}'"),
732                }
733            }
734            (Some(OutputID::Private(output_hash)), Value::Plaintext(plaintext)) => {
735                // Construct the (console) output index as a field element.
736                let index = Field::from_u16(u16::try_from(num_inputs + index)?);
737                // Compute the ciphertext, with the input view key as `Hash(function ID || tvk || index)`.
738                let ciphertext = plaintext.encrypt_symmetric(N::hash_psd4(&[function_id, *tvk, index])?)?;
739                // Compute the ciphertext hash.
740                let ciphertext_hash = N::hash_psd8(&ciphertext.to_fields()?)?;
741                // Ensure the ciphertext hash matches.
742                ensure!(*output_hash == ciphertext_hash, "The output ciphertext hash is incorrect");
743                // Return the private output.
744                Ok(Output::Private(*output_hash, Some(ciphertext)))
745            }
746            (Some(OutputID::Record(commitment, checksum, sender_ciphertext)), Value::Record(record)) => {
747                // Retrieve the record name.
748                let record_name = match output_type {
749                    ValueType::Record(record_name) => record_name,
750                    // Ensure the input type is a record.
751                    _ => bail!("Expected a record type at output {index}"),
752                };
753
754                // Retrieve the output register.
755                let output_register = match output_register {
756                    Some(output_register) => output_register,
757                    None => bail!("Expected a register to be paired with a record output"),
758                };
759
760                // Construct the (console) output index as a field element.
761                let output_index = Field::from_u64(output_register.locator());
762                // Compute the encryption randomizer as `HashToScalar(tvk || index)`.
763                let randomizer = N::hash_to_scalar_psd2(&[*tvk, output_index])?;
764
765                // Encrypt the record, using the randomizer.
766                let (record_ciphertext, record_view_key) = record.encrypt_symmetric(randomizer)?;
767
768                // Compute the record commitment.
769                let candidate_cm = record.to_commitment(program_id, record_name, &record_view_key)?;
770                // Ensure the commitment matches.
771                ensure!(*commitment == candidate_cm, "The output record commitment is incorrect");
772
773                // Compute the record checksum, as the hash of the encrypted record.
774                let ciphertext_checksum = N::hash_bhp1024(&record_ciphertext.to_bits_le())?;
775                // Ensure the checksum matches.
776                ensure!(*checksum == ciphertext_checksum, "The output record ciphertext checksum is incorrect");
777
778                // Prepare a randomizer for the sender ciphertext.
779                let randomizer = N::hash_psd4(&[N::encryption_domain(), record_view_key, Field::one()])?;
780                // Encrypt the signer address using the randomizer.
781                let candidate_sender_ciphertext = (**signer).to_x_coordinate() + randomizer;
782                // Ensure the sender ciphertext matches, or the sender ciphertext is zero.
783                // Note: The option to allow a zero-value in the sender ciphertext allows
784                // this feature to become optional or deactivated in the future.
785                ensure!(
786                    (*sender_ciphertext == candidate_sender_ciphertext) || sender_ciphertext.is_zero(),
787                    "The output record sender ciphertext is incorrect"
788                );
789
790                // Check if caller sees this as a dynamic record.
791                if let Some(Value::DynamicRecord(dynamic_record)) = caller_value {
792                    // Compute the dynamic ID.
793                    let dynamic_id = compute_output_hash(function_id, dynamic_record, tvk, num_inputs, index)?;
794                    // Return the record with dynamic ID.
795                    Ok(Output::RecordWithDynamicID(
796                        *commitment,
797                        *checksum,
798                        Some(record_ciphertext),
799                        Some(*sender_ciphertext),
800                        dynamic_id,
801                    ))
802                } else {
803                    // Return the record output.
804                    Ok(Output::Record(*commitment, *checksum, Some(record_ciphertext), Some(*sender_ciphertext)))
805                }
806            }
807            (Some(OutputID::ExternalRecord(hash)), Value::Record(record)) => {
808                // Compute the candidate hash.
809                let candidate_hash = compute_output_hash(function_id, record, tvk, num_inputs, index)?;
810                // Ensure the hash matches.
811                ensure!(*hash == candidate_hash, "The output external hash is incorrect");
812
813                // Check if caller sees this as a dynamic record.
814                if let Some(Value::DynamicRecord(dynamic_record)) = caller_value {
815                    // Compute the dynamic ID.
816                    let dynamic_id = compute_output_hash(function_id, dynamic_record, tvk, num_inputs, index)?;
817                    // Return the external record with dynamic ID.
818                    Ok(Output::ExternalRecordWithDynamicID(*hash, dynamic_id))
819                } else {
820                    // Return the external record output.
821                    Ok(Output::ExternalRecord(*hash))
822                }
823            }
824            (Some(OutputID::Future(output_hash)), Value::Future(future)) => {
825                // Construct the future output.
826                let output = Output::Future(*output_hash, Some(future.clone()));
827                // Ensure the output is valid.
828                match output.verify(function_id, tcm, num_inputs + index) {
829                    true => Ok(output),
830                    false => bail!("Malformed future transition output: '{output}'"),
831                }
832            }
833            (Some(OutputID::DynamicRecord(hash)), Value::DynamicRecord(dynamic_record)) => {
834                // Compute the candidate hash.
835                let candidate_hash = compute_output_hash(function_id, dynamic_record, tvk, num_inputs, index)?;
836                // Ensure the hash matches.
837                ensure!(*hash == candidate_hash, "The output dynamic record hash is incorrect");
838                // Return the dynamic record output.
839                Ok(Output::DynamicRecord(*hash))
840            }
841            (None, Value::DynamicRecord(dynamic_record)) => {
842                // Compute the hash.
843                let hash = compute_output_hash(function_id, dynamic_record, tvk, num_inputs, index)?;
844                // Return the dynamic record output.
845                Ok(Output::DynamicRecord(hash))
846            }
847            _ => bail!("Malformed response output: {output_id:?}, {output}"),
848        }
849    }
850}
851
852#[cfg(test)]
853pub mod test_helpers {
854    use super::*;
855    use crate::Transaction;
856
857    type CurrentNetwork = console::network::MainnetV0;
858
859    /// Samples a random transition.
860    pub(crate) fn sample_transition(rng: &mut TestRng) -> Transition<CurrentNetwork> {
861        if let Transaction::Execute(_, _, execution, _) =
862            crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng, 0)
863        {
864            execution.into_transitions().next().unwrap()
865        } else {
866            unreachable!()
867        }
868    }
869}