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), 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                        // Compute the record commitment.
209                        let candidate_cm = record.to_commitment(&program_id, record_name)?;
210                        // Ensure the commitment matches.
211                        ensure!(*commitment == candidate_cm, "The output record commitment is incorrect");
212
213                        // Construct the (console) output index as a field element.
214                        let index = Field::from_u64(output_register.locator());
215                        // Compute the encryption randomizer as `HashToScalar(tvk || index)`.
216                        let randomizer = N::hash_to_scalar_psd2(&[*request.tvk(), index])?;
217
218                        // Encrypt the record, using the randomizer.
219                        let record_ciphertext = record.encrypt(randomizer)?;
220                        // Compute the record checksum, as the hash of the encrypted record.
221                        let ciphertext_checksum = N::hash_bhp1024(&record_ciphertext.to_bits_le())?;
222                        // Ensure the checksum matches.
223                        ensure!(*checksum == ciphertext_checksum, "The output record ciphertext checksum is incorrect");
224
225                        // Return the record output.
226                        Ok(Output::Record(*commitment, *checksum, Some(record_ciphertext)))
227                    }
228                    (OutputID::ExternalRecord(hash), Value::Record(record)) => {
229                        // Construct the (console) output index as a field element.
230                        let index = Field::from_u16(u16::try_from(num_inputs + index)?);
231                        // Construct the preimage as `(function ID || output || tvk || index)`.
232                        let mut preimage = Vec::new();
233                        preimage.push(function_id);
234                        preimage.extend(record.to_fields()?);
235                        preimage.push(*request.tvk());
236                        preimage.push(index);
237                        // Hash the output to a field element.
238                        let candidate_hash = N::hash_psd8(&preimage)?;
239                        // Ensure the hash matches.
240                        ensure!(*hash == candidate_hash, "The output external hash is incorrect");
241                        // Return the record output.
242                        Ok(Output::ExternalRecord(*hash))
243                    }
244                    (OutputID::Future(output_hash), Value::Future(future)) => {
245                        // Construct the future output.
246                        let output = Output::Future(*output_hash, Some(future.clone()));
247                        // Ensure the output is valid.
248                        match output.verify(function_id, request.tcm(), num_inputs + index) {
249                            true => Ok(output),
250                            false => bail!("Malformed future transition output: '{output}'"),
251                        }
252                    }
253                    _ => bail!("Malformed response output: {output_id:?}, {output}"),
254                }
255            })
256            .collect::<Result<Vec<_>>>()?;
257
258        // Retrieve the `tpk`.
259        let tpk = request.to_tpk();
260        // Retrieve the `tcm`.
261        let tcm = *request.tcm();
262        // Retrieve the `scm`.
263        let scm = *request.scm();
264        // Return the transition.
265        Self::new(program_id, function_name, inputs, outputs, tpk, tcm, scm)
266    }
267}
268
269impl<N: Network> Transition<N> {
270    /// Returns the transition ID.
271    pub const fn id(&self) -> &N::TransitionID {
272        &self.id
273    }
274
275    /// Returns the program ID.
276    pub const fn program_id(&self) -> &ProgramID<N> {
277        &self.program_id
278    }
279
280    /// Returns the function name.
281    pub const fn function_name(&self) -> &Identifier<N> {
282        &self.function_name
283    }
284
285    /// Returns the inputs.
286    pub fn inputs(&self) -> &[Input<N>] {
287        &self.inputs
288    }
289
290    /// Return the outputs.
291    pub fn outputs(&self) -> &[Output<N>] {
292        &self.outputs
293    }
294
295    /// Returns the transition public key.
296    pub const fn tpk(&self) -> &Group<N> {
297        &self.tpk
298    }
299
300    /// Returns the transition commitment.
301    pub const fn tcm(&self) -> &Field<N> {
302        &self.tcm
303    }
304
305    /// Returns the signer commitment.
306    pub const fn scm(&self) -> &Field<N> {
307        &self.scm
308    }
309}
310
311impl<N: Network> Transition<N> {
312    /// Returns `true` if this is a `bond_public` transition.
313    #[inline]
314    pub fn is_bond_public(&self) -> bool {
315        self.inputs.len() == 3
316            && self.outputs.len() == 1
317            && self.program_id.to_string() == "credits.aleo"
318            && self.function_name.to_string() == "bond_public"
319    }
320
321    /// Returns `true` if this is a `bond_validator` transition.
322    #[inline]
323    pub fn is_bond_validator(&self) -> bool {
324        self.inputs.len() == 3
325            && self.outputs.len() == 1
326            && self.program_id.to_string() == "credits.aleo"
327            && self.function_name.to_string() == "bond_validator"
328    }
329
330    /// Returns `true` if this is an `unbond_public` transition.
331    #[inline]
332    pub fn is_unbond_public(&self) -> bool {
333        self.inputs.len() == 2
334            && self.outputs.len() == 1
335            && self.program_id.to_string() == "credits.aleo"
336            && self.function_name.to_string() == "unbond_public"
337    }
338
339    /// Returns `true` if this is a `fee_private` transition.
340    #[inline]
341    pub fn is_fee_private(&self) -> bool {
342        self.inputs.len() == 4
343            && self.outputs.len() == 1
344            && self.program_id.to_string() == "credits.aleo"
345            && self.function_name.to_string() == "fee_private"
346    }
347
348    /// Returns `true` if this is a `fee_public` transition.
349    #[inline]
350    pub fn is_fee_public(&self) -> bool {
351        self.inputs.len() == 3
352            && self.outputs.len() == 1
353            && self.program_id.to_string() == "credits.aleo"
354            && self.function_name.to_string() == "fee_public"
355    }
356
357    /// Returns `true` if this is a `split` transition.
358    #[inline]
359    pub fn is_split(&self) -> bool {
360        self.inputs.len() == 2
361            && self.outputs.len() == 2
362            && self.program_id.to_string() == "credits.aleo"
363            && self.function_name.to_string() == "split"
364    }
365}
366
367impl<N: Network> Transition<N> {
368    /// Returns `true` if the transition contains the given serial number.
369    pub fn contains_serial_number(&self, serial_number: &Field<N>) -> bool {
370        self.inputs.iter().any(|input| match input {
371            Input::Constant(_, _) => false,
372            Input::Public(_, _) => false,
373            Input::Private(_, _) => false,
374            Input::Record(input_sn, _) => input_sn == serial_number,
375            Input::ExternalRecord(_) => false,
376        })
377    }
378
379    /// Returns `true` if the transition contains the given commitment.
380    pub fn contains_commitment(&self, commitment: &Field<N>) -> bool {
381        self.outputs.iter().any(|output| match output {
382            Output::Constant(_, _) => false,
383            Output::Public(_, _) => false,
384            Output::Private(_, _) => false,
385            Output::Record(output_cm, _, _) => output_cm == commitment,
386            Output::ExternalRecord(_) => false,
387            Output::Future(_, _) => false,
388        })
389    }
390}
391
392impl<N: Network> Transition<N> {
393    /// Returns the record with the corresponding commitment, if it exists.
394    pub fn find_record(&self, commitment: &Field<N>) -> Option<&Record<N, Ciphertext<N>>> {
395        self.outputs.iter().find_map(|output| match output {
396            Output::Constant(_, _) => None,
397            Output::Public(_, _) => None,
398            Output::Private(_, _) => None,
399            Output::Record(output_cm, _, Some(record)) if output_cm == commitment => Some(record),
400            Output::Record(_, _, _) => None,
401            Output::ExternalRecord(_) => None,
402            Output::Future(_, _) => None,
403        })
404    }
405}
406
407impl<N: Network> Transition<N> {
408    /* Input */
409
410    /// Returns the input IDs.
411    pub fn input_ids(&self) -> impl '_ + ExactSizeIterator<Item = &Field<N>> {
412        self.inputs.iter().map(Input::id)
413    }
414
415    /// Returns an iterator over the serial numbers, for inputs that are records.
416    pub fn serial_numbers(&self) -> impl '_ + Iterator<Item = &Field<N>> {
417        self.inputs.iter().flat_map(Input::serial_number)
418    }
419
420    /// Returns an iterator over the tags, for inputs that are records.
421    pub fn tags(&self) -> impl '_ + Iterator<Item = &Field<N>> {
422        self.inputs.iter().flat_map(Input::tag)
423    }
424
425    /* Output */
426
427    /// Returns the output IDs.
428    pub fn output_ids(&self) -> impl '_ + ExactSizeIterator<Item = &Field<N>> {
429        self.outputs.iter().map(Output::id)
430    }
431
432    /// Returns an iterator over the commitments, for outputs that are records.
433    pub fn commitments(&self) -> impl '_ + Iterator<Item = &Field<N>> {
434        self.outputs.iter().flat_map(Output::commitment)
435    }
436
437    /// Returns an iterator over the nonces, for outputs that are records.
438    pub fn nonces(&self) -> impl '_ + Iterator<Item = &Group<N>> {
439        self.outputs.iter().flat_map(Output::nonce)
440    }
441
442    /// Returns an iterator over the output records, as a tuple of `(commitment, record)`.
443    pub fn records(&self) -> impl '_ + Iterator<Item = (&Field<N>, &Record<N, Ciphertext<N>>)> {
444        self.outputs.iter().flat_map(Output::record)
445    }
446}
447
448impl<N: Network> Transition<N> {
449    /// Returns the transition ID, and consumes `self`.
450    pub fn into_id(self) -> N::TransitionID {
451        self.id
452    }
453
454    /* Input */
455
456    /// Returns a consuming iterator over the serial numbers, for inputs that are records.
457    pub fn into_serial_numbers(self) -> impl Iterator<Item = Field<N>> {
458        self.inputs.into_iter().flat_map(Input::into_serial_number)
459    }
460
461    /// Returns a consuming iterator over the tags, for inputs that are records.
462    pub fn into_tags(self) -> impl Iterator<Item = Field<N>> {
463        self.inputs.into_iter().flat_map(Input::into_tag)
464    }
465
466    /* Output */
467
468    /// Returns a consuming iterator over the commitments, for outputs that are records.
469    pub fn into_commitments(self) -> impl Iterator<Item = Field<N>> {
470        self.outputs.into_iter().flat_map(Output::into_commitment)
471    }
472
473    /// Returns a consuming iterator over the nonces, for outputs that are records.
474    pub fn into_nonces(self) -> impl Iterator<Item = Group<N>> {
475        self.outputs.into_iter().flat_map(Output::into_nonce)
476    }
477
478    /// Returns a consuming iterator over the output records, as a tuple of `(commitment, record)`.
479    pub fn into_records(self) -> impl Iterator<Item = (Field<N>, Record<N, Ciphertext<N>>)> {
480        self.outputs.into_iter().flat_map(Output::into_record)
481    }
482
483    /// Returns the transition public key, and consumes `self`.
484    pub fn into_tpk(self) -> Group<N> {
485        self.tpk
486    }
487}
488
489#[cfg(test)]
490pub mod test_helpers {
491    use super::*;
492    use crate::Transaction;
493
494    type CurrentNetwork = console::network::MainnetV0;
495
496    /// Samples a random transition.
497    pub(crate) fn sample_transition(rng: &mut TestRng) -> Transition<CurrentNetwork> {
498        if let Transaction::Execute(_, _, execution, _) =
499            crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng)
500        {
501            execution.into_transitions().next().unwrap()
502        } else {
503            unreachable!()
504        }
505    }
506}