snarkvm_ledger_block/transition/
mod.rs

1// Copyright (c) 2019-2025 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        TransitionLeaf,
41        TransitionPath,
42        TransitionTree,
43        Value,
44        ValueType,
45        compute_function_id,
46    },
47    types::{Field, Group},
48};
49
50#[derive(Clone, PartialEq, Eq)]
51pub struct Transition<N: Network> {
52    /// The transition ID.
53    id: N::TransitionID,
54    /// The program ID.
55    program_id: ProgramID<N>,
56    /// The function name.
57    function_name: Identifier<N>,
58    /// The transition inputs.
59    inputs: Vec<Input<N>>,
60    /// The transition outputs.
61    outputs: Vec<Output<N>>,
62    /// The transition public key.
63    tpk: Group<N>,
64    /// The transition commitment.
65    tcm: Field<N>,
66    /// The transition signer commitment.
67    scm: Field<N>,
68}
69
70impl<N: Network> Transition<N> {
71    /// Initializes a new transition.
72    #[allow(clippy::too_many_arguments)]
73    pub fn new(
74        program_id: ProgramID<N>,
75        function_name: Identifier<N>,
76        inputs: Vec<Input<N>>,
77        outputs: Vec<Output<N>>,
78        tpk: Group<N>,
79        tcm: Field<N>,
80        scm: Field<N>,
81    ) -> Result<Self> {
82        // Compute the transition ID.
83        let function_tree = Self::function_tree(&inputs, &outputs)?;
84        let id = N::hash_bhp512(&(*function_tree.root(), tcm).to_bits_le())?;
85        // Return the transition.
86        Ok(Self { id: id.into(), program_id, function_name, inputs, outputs, tpk, tcm, scm })
87    }
88
89    /// Initializes a new transition from a request and response.
90    pub fn from(
91        request: &Request<N>,
92        response: &Response<N>,
93        output_types: &[ValueType<N>],
94        output_registers: &[Option<Register<N>>],
95    ) -> Result<Self> {
96        let network_id = *request.network_id();
97        let program_id = *request.program_id();
98        let function_name = *request.function_name();
99        let num_inputs = request.inputs().len();
100
101        // Compute the function ID.
102        let function_id = compute_function_id(&network_id, &program_id, &function_name)?;
103
104        let inputs = request
105            .input_ids()
106            .iter()
107            .zip_eq(request.inputs())
108            .enumerate()
109            .map(|(index, (input_id, input))| {
110                // Construct the transition input.
111                match (input_id, input) {
112                    (InputID::Constant(input_hash), Value::Plaintext(plaintext)) => {
113                        // Construct the constant input.
114                        let input = Input::Constant(*input_hash, Some(plaintext.clone()));
115                        // Ensure the input is valid.
116                        match input.verify(function_id, request.tcm(), index) {
117                            true => Ok(input),
118                            false => bail!("Malformed constant transition input: '{input}'"),
119                        }
120                    }
121                    (InputID::Public(input_hash), Value::Plaintext(plaintext)) => {
122                        // Construct the public input.
123                        let input = Input::Public(*input_hash, Some(plaintext.clone()));
124                        // Ensure the input is valid.
125                        match input.verify(function_id, request.tcm(), index) {
126                            true => Ok(input),
127                            false => bail!("Malformed public transition input: '{input}'"),
128                        }
129                    }
130                    (InputID::Private(input_hash), Value::Plaintext(plaintext)) => {
131                        // Construct the (console) input index as a field element.
132                        let index = Field::from_u16(index as u16);
133                        // Compute the ciphertext, with the input view key as `Hash(function ID || tvk || index)`.
134                        let ciphertext =
135                            plaintext.encrypt_symmetric(N::hash_psd4(&[function_id, *request.tvk(), index])?)?;
136                        // Compute the ciphertext hash.
137                        let ciphertext_hash = N::hash_psd8(&ciphertext.to_fields()?)?;
138                        // Ensure the ciphertext hash matches.
139                        ensure!(*input_hash == ciphertext_hash, "The input ciphertext hash is incorrect");
140                        // Return the private input.
141                        Ok(Input::Private(*input_hash, Some(ciphertext)))
142                    }
143                    (InputID::Record(_, _, _, serial_number, tag), Value::Record(..)) => {
144                        // Return the input record.
145                        Ok(Input::Record(*serial_number, *tag))
146                    }
147                    (InputID::ExternalRecord(input_hash), Value::Record(..)) => Ok(Input::ExternalRecord(*input_hash)),
148                    _ => bail!("Malformed request input: {:?}, {input}", input_id),
149                }
150            })
151            .collect::<Result<Vec<_>>>()?;
152
153        let outputs = response
154            .output_ids()
155            .iter()
156            .zip_eq(response.outputs())
157            .zip_eq(output_types)
158            .zip_eq(output_registers)
159            .enumerate()
160            .map(|(index, (((output_id, output), output_type), output_register))| {
161                // Construct the transition output.
162                match (output_id, output) {
163                    (OutputID::Constant(output_hash), Value::Plaintext(plaintext)) => {
164                        // Construct the constant output.
165                        let output = Output::Constant(*output_hash, Some(plaintext.clone()));
166                        // Ensure the output is valid.
167                        match output.verify(function_id, request.tcm(), num_inputs + index) {
168                            true => Ok(output),
169                            false => bail!("Malformed constant transition output: '{output}'"),
170                        }
171                    }
172                    (OutputID::Public(output_hash), Value::Plaintext(plaintext)) => {
173                        // Construct the public output.
174                        let output = Output::Public(*output_hash, Some(plaintext.clone()));
175                        // Ensure the output is valid.
176                        match output.verify(function_id, request.tcm(), num_inputs + index) {
177                            true => Ok(output),
178                            false => bail!("Malformed public transition output: '{output}'"),
179                        }
180                    }
181                    (OutputID::Private(output_hash), Value::Plaintext(plaintext)) => {
182                        // Construct the (console) output index as a field element.
183                        let index = Field::from_u16(u16::try_from(num_inputs + index)?);
184                        // Compute the ciphertext, with the input view key as `Hash(function ID || tvk || index)`.
185                        let ciphertext =
186                            plaintext.encrypt_symmetric(N::hash_psd4(&[function_id, *request.tvk(), index])?)?;
187                        // Compute the ciphertext hash.
188                        let ciphertext_hash = N::hash_psd8(&ciphertext.to_fields()?)?;
189                        // Ensure the ciphertext hash matches.
190                        ensure!(*output_hash == ciphertext_hash, "The output ciphertext hash is incorrect");
191                        // Return the private output.
192                        Ok(Output::Private(*output_hash, Some(ciphertext)))
193                    }
194                    (OutputID::Record(commitment, checksum, sender_ciphertext), Value::Record(record)) => {
195                        // Retrieve the record name.
196                        let record_name = match output_type {
197                            ValueType::Record(record_name) => record_name,
198                            // Ensure the input type is a record.
199                            _ => bail!("Expected a record type at output {index}"),
200                        };
201
202                        // Retrieve the output register.
203                        let output_register = match output_register {
204                            Some(output_register) => output_register,
205                            None => bail!("Expected a register to be paired with a record output"),
206                        };
207
208                        // Construct the (console) output index as a field element.
209                        let index = Field::from_u64(output_register.locator());
210                        // Compute the encryption randomizer as `HashToScalar(tvk || index)`.
211                        let randomizer = N::hash_to_scalar_psd2(&[*request.tvk(), index])?;
212
213                        // Encrypt the record, using the randomizer.
214                        let (record_ciphertext, record_view_key) = record.encrypt_symmetric(randomizer)?;
215
216                        // Compute the record commitment.
217                        let candidate_cm = record.to_commitment(&program_id, record_name, &record_view_key)?;
218                        // Ensure the commitment matches.
219                        ensure!(*commitment == candidate_cm, "The output record commitment is incorrect");
220
221                        // Compute the record checksum, as the hash of the encrypted record.
222                        let ciphertext_checksum = N::hash_bhp1024(&record_ciphertext.to_bits_le())?;
223                        // Ensure the checksum matches.
224                        ensure!(*checksum == ciphertext_checksum, "The output record ciphertext checksum is incorrect");
225
226                        // Prepare a randomizer for the sender ciphertext.
227                        let randomizer = N::hash_psd4(&[N::encryption_domain(), record_view_key, Field::one()])?;
228                        // Encrypt the signer address using the randomizer.
229                        let candidate_sender_ciphertext = (**request.signer()).to_x_coordinate() + randomizer;
230                        // Ensure the sender ciphertext matches, or the sender ciphertext is zero.
231                        // Note: The option to allow a zero-value in the sender ciphertext allows
232                        // this feature to become optional or deactivated in the future.
233                        ensure!(
234                            (*sender_ciphertext == candidate_sender_ciphertext) || sender_ciphertext.is_zero(),
235                            "The output record sender ciphertext is incorrect"
236                        );
237
238                        // Return the record output.
239                        Ok(Output::Record(*commitment, *checksum, Some(record_ciphertext), Some(*sender_ciphertext)))
240                    }
241                    (OutputID::ExternalRecord(hash), Value::Record(record)) => {
242                        // Construct the (console) output index as a field element.
243                        let index = Field::from_u16(u16::try_from(num_inputs + index)?);
244                        // Construct the preimage as `(function ID || output || tvk || index)`.
245                        let mut preimage = Vec::new();
246                        preimage.push(function_id);
247                        preimage.extend(record.to_fields()?);
248                        preimage.push(*request.tvk());
249                        preimage.push(index);
250                        // Hash the output to a field element.
251                        let candidate_hash = N::hash_psd8(&preimage)?;
252                        // Ensure the hash matches.
253                        ensure!(*hash == candidate_hash, "The output external hash is incorrect");
254                        // Return the record output.
255                        Ok(Output::ExternalRecord(*hash))
256                    }
257                    (OutputID::Future(output_hash), Value::Future(future)) => {
258                        // Construct the future output.
259                        let output = Output::Future(*output_hash, Some(future.clone()));
260                        // Ensure the output is valid.
261                        match output.verify(function_id, request.tcm(), num_inputs + index) {
262                            true => Ok(output),
263                            false => bail!("Malformed future transition output: '{output}'"),
264                        }
265                    }
266                    _ => bail!("Malformed response output: {output_id:?}, {output}"),
267                }
268            })
269            .collect::<Result<Vec<_>>>()?;
270
271        // Retrieve the `tpk`.
272        let tpk = request.to_tpk();
273        // Retrieve the `tcm`.
274        let tcm = *request.tcm();
275        // Retrieve the `scm`.
276        let scm = *request.scm();
277        // Return the transition.
278        Self::new(program_id, function_name, inputs, outputs, tpk, tcm, scm)
279    }
280}
281
282impl<N: Network> Transition<N> {
283    /// Returns the transition ID.
284    pub const fn id(&self) -> &N::TransitionID {
285        &self.id
286    }
287
288    /// Returns the program ID.
289    pub const fn program_id(&self) -> &ProgramID<N> {
290        &self.program_id
291    }
292
293    /// Returns the function name.
294    pub const fn function_name(&self) -> &Identifier<N> {
295        &self.function_name
296    }
297
298    /// Returns the inputs.
299    pub fn inputs(&self) -> &[Input<N>] {
300        &self.inputs
301    }
302
303    /// Return the outputs.
304    pub fn outputs(&self) -> &[Output<N>] {
305        &self.outputs
306    }
307
308    /// Returns the transition public key.
309    pub const fn tpk(&self) -> &Group<N> {
310        &self.tpk
311    }
312
313    /// Returns the transition commitment.
314    pub const fn tcm(&self) -> &Field<N> {
315        &self.tcm
316    }
317
318    /// Returns the signer commitment.
319    pub const fn scm(&self) -> &Field<N> {
320        &self.scm
321    }
322}
323
324impl<N: Network> Transition<N> {
325    /// Returns `true` if this is a `credits.aleo/*` transition.
326    #[inline]
327    pub fn is_credits(&self) -> bool {
328        self.program_id.to_string() == "credits.aleo"
329    }
330
331    /// Returns `true` if this is a `bond_public` transition.
332    #[inline]
333    pub fn is_bond_public(&self) -> bool {
334        self.inputs.len() == 3
335            && self.outputs.len() == 1
336            && self.program_id.to_string() == "credits.aleo"
337            && self.function_name.to_string() == "bond_public"
338    }
339
340    /// Returns `true` if this is a `bond_validator` transition.
341    #[inline]
342    pub fn is_bond_validator(&self) -> bool {
343        self.inputs.len() == 3
344            && self.outputs.len() == 1
345            && self.program_id.to_string() == "credits.aleo"
346            && self.function_name.to_string() == "bond_validator"
347    }
348
349    /// Returns `true` if this is an `unbond_public` transition.
350    #[inline]
351    pub fn is_unbond_public(&self) -> bool {
352        self.inputs.len() == 2
353            && self.outputs.len() == 1
354            && self.program_id.to_string() == "credits.aleo"
355            && self.function_name.to_string() == "unbond_public"
356    }
357
358    /// Returns `true` if this is a `fee_private` transition.
359    #[inline]
360    pub fn is_fee_private(&self) -> bool {
361        self.inputs.len() == 4
362            && self.outputs.len() == 1
363            && self.program_id.to_string() == "credits.aleo"
364            && self.function_name.to_string() == "fee_private"
365    }
366
367    /// Returns `true` if this is a `fee_public` transition.
368    #[inline]
369    pub fn is_fee_public(&self) -> bool {
370        self.inputs.len() == 3
371            && self.outputs.len() == 1
372            && self.program_id.to_string() == "credits.aleo"
373            && self.function_name.to_string() == "fee_public"
374    }
375
376    /// Returns `true` if this is a `split` transition.
377    #[inline]
378    pub fn is_split(&self) -> bool {
379        self.inputs.len() == 2
380            && self.outputs.len() == 2
381            && self.program_id.to_string() == "credits.aleo"
382            && self.function_name.to_string() == "split"
383    }
384
385    /// Returns `true` if this is an `upgrade` transition.
386    #[inline]
387    pub fn is_upgrade(&self) -> bool {
388        self.inputs.len() == 1
389            && self.outputs.len() == 2
390            && self.program_id.to_string() == "credits.aleo"
391            && self.function_name.to_string() == "upgrade"
392    }
393}
394
395impl<N: Network> Transition<N> {
396    /// Returns `true` if the transition contains the given serial number.
397    pub fn contains_serial_number(&self, serial_number: &Field<N>) -> bool {
398        self.inputs.iter().any(|input| match input {
399            Input::Constant(_, _) => false,
400            Input::Public(_, _) => false,
401            Input::Private(_, _) => false,
402            Input::Record(input_sn, _) => input_sn == serial_number,
403            Input::ExternalRecord(_) => false,
404        })
405    }
406
407    /// Returns `true` if the transition contains the given commitment.
408    pub fn contains_commitment(&self, commitment: &Field<N>) -> bool {
409        self.outputs.iter().any(|output| match output {
410            Output::Constant(_, _) => false,
411            Output::Public(_, _) => false,
412            Output::Private(_, _) => false,
413            Output::Record(output_cm, _, _, _) => output_cm == commitment,
414            Output::ExternalRecord(_) => false,
415            Output::Future(_, _) => false,
416        })
417    }
418}
419
420impl<N: Network> Transition<N> {
421    /// Returns the record with the corresponding commitment, if it exists.
422    pub fn find_record(&self, commitment: &Field<N>) -> Option<&Record<N, Ciphertext<N>>> {
423        self.outputs.iter().find_map(|output| match output {
424            Output::Constant(_, _) => None,
425            Output::Public(_, _) => None,
426            Output::Private(_, _) => None,
427            Output::Record(output_cm, _, Some(record), _) if output_cm == commitment => Some(record),
428            Output::Record(_, _, _, _) => None,
429            Output::ExternalRecord(_) => None,
430            Output::Future(_, _) => None,
431        })
432    }
433}
434
435impl<N: Network> Transition<N> {
436    /* Input */
437
438    /// Returns the input IDs.
439    pub fn input_ids(&self) -> impl '_ + ExactSizeIterator<Item = &Field<N>> {
440        self.inputs.iter().map(Input::id)
441    }
442
443    /// Returns an iterator over the serial numbers, for inputs that are records.
444    pub fn serial_numbers(&self) -> impl '_ + Iterator<Item = &Field<N>> {
445        self.inputs.iter().flat_map(Input::serial_number)
446    }
447
448    /// Returns an iterator over the tags, for inputs that are records.
449    pub fn tags(&self) -> impl '_ + Iterator<Item = &Field<N>> {
450        self.inputs.iter().flat_map(Input::tag)
451    }
452
453    /* Output */
454
455    /// Returns the output IDs.
456    pub fn output_ids(&self) -> impl '_ + ExactSizeIterator<Item = &Field<N>> {
457        self.outputs.iter().map(Output::id)
458    }
459
460    /// Returns an iterator over the commitments, for outputs that are records.
461    pub fn commitments(&self) -> impl '_ + Iterator<Item = &Field<N>> {
462        self.outputs.iter().flat_map(Output::commitment)
463    }
464
465    /// Returns an iterator over the nonces, for outputs that are records.
466    pub fn nonces(&self) -> impl '_ + Iterator<Item = &Group<N>> {
467        self.outputs.iter().flat_map(Output::nonce)
468    }
469
470    /// Returns an iterator over the output records, as a tuple of `(commitment, record)`.
471    pub fn records(&self) -> impl '_ + Iterator<Item = (&Field<N>, &Record<N, Ciphertext<N>>)> {
472        self.outputs.iter().flat_map(Output::record)
473    }
474}
475
476impl<N: Network> Transition<N> {
477    /// Returns the transition ID, and consumes `self`.
478    pub fn into_id(self) -> N::TransitionID {
479        self.id
480    }
481
482    /* Input */
483
484    /// Returns a consuming iterator over the serial numbers, for inputs that are records.
485    pub fn into_serial_numbers(self) -> impl Iterator<Item = Field<N>> {
486        self.inputs.into_iter().flat_map(Input::into_serial_number)
487    }
488
489    /// Returns a consuming iterator over the tags, for inputs that are records.
490    pub fn into_tags(self) -> impl Iterator<Item = Field<N>> {
491        self.inputs.into_iter().flat_map(Input::into_tag)
492    }
493
494    /* Output */
495
496    /// Returns a consuming iterator over the commitments, for outputs that are records.
497    pub fn into_commitments(self) -> impl Iterator<Item = Field<N>> {
498        self.outputs.into_iter().flat_map(Output::into_commitment)
499    }
500
501    /// Returns a consuming iterator over the nonces, for outputs that are records.
502    pub fn into_nonces(self) -> impl Iterator<Item = Group<N>> {
503        self.outputs.into_iter().flat_map(Output::into_nonce)
504    }
505
506    /// Returns a consuming iterator over the output records, as a tuple of `(commitment, record)`.
507    pub fn into_records(self) -> impl Iterator<Item = (Field<N>, Record<N, Ciphertext<N>>)> {
508        self.outputs.into_iter().flat_map(Output::into_record)
509    }
510
511    /// Returns the transition public key, and consumes `self`.
512    pub fn into_tpk(self) -> Group<N> {
513        self.tpk
514    }
515}
516
517#[cfg(test)]
518pub mod test_helpers {
519    use super::*;
520    use crate::Transaction;
521
522    type CurrentNetwork = console::network::MainnetV0;
523
524    /// Samples a random transition.
525    pub(crate) fn sample_transition(rng: &mut TestRng) -> Transition<CurrentNetwork> {
526        if let Transaction::Execute(_, _, execution, _) =
527            crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng)
528        {
529            execution.into_transitions().next().unwrap()
530        } else {
531            unreachable!()
532        }
533    }
534}