Skip to main content

snarkvm_ledger_block/transition/output/
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
16mod bytes;
17mod serialize;
18mod string;
19
20use console::{
21    account::{Address, ViewKey},
22    network::prelude::*,
23    program::{Ciphertext, Future, Plaintext, Record, TransitionLeaf, ValueType},
24    types::{Field, Group},
25};
26
27type Variant = u8;
28
29/// The transition output.
30#[derive(Clone, PartialEq, Eq)]
31pub enum Output<N: Network> {
32    /// The plaintext hash and (optional) plaintext.
33    Constant(Field<N>, Option<Plaintext<N>>),
34    /// The plaintext hash and (optional) plaintext.
35    Public(Field<N>, Option<Plaintext<N>>),
36    /// The ciphertext hash and (optional) ciphertext.
37    Private(Field<N>, Option<Ciphertext<N>>),
38    /// The commitment, checksum, (optional) record ciphertext, and (optional) sender ciphertext.
39    Record(Field<N>, Field<N>, Option<Record<N, Ciphertext<N>>>, Option<Field<N>>),
40    /// The hash of the external record's (function_id, record, tvk, output index).
41    ExternalRecord(Field<N>),
42    /// The future hash and (optional) future.
43    Future(Field<N>, Option<Future<N>>),
44    /// The hash of the dynamic record's (function_id, record, tvk, output index).
45    DynamicRecord(Field<N>),
46    /// The commitment, checksum, (optional) record ciphertext, (optional) sender ciphertext, and dynamic ID.
47    RecordWithDynamicID(Field<N>, Field<N>, Option<Record<N, Ciphertext<N>>>, Option<Field<N>>, Field<N>),
48    /// The external record hash and dynamic ID.
49    ExternalRecordWithDynamicID(Field<N>, Field<N>),
50}
51
52impl<N: Network> Output<N> {
53    /// Returns the variant of the output.
54    pub const fn variant(&self) -> Variant {
55        match self {
56            Output::Constant(_, _) => 0,
57            Output::Public(_, _) => 1,
58            Output::Private(_, _) => 2,
59            Output::Record(_, _, _, _) => 3,
60            Output::ExternalRecord(_) => 4,
61            Output::Future(_, _) => 5,
62            Output::DynamicRecord(_) => 6,
63            Output::RecordWithDynamicID(..) => 7,
64            Output::ExternalRecordWithDynamicID(..) => 8,
65        }
66    }
67
68    /// Returns the ID of the output.
69    pub const fn id(&self) -> &Field<N> {
70        match self {
71            Output::Constant(id, ..) => id,
72            Output::Public(id, ..) => id,
73            Output::Private(id, ..) => id,
74            Output::Record(commitment, ..) => commitment,
75            Output::ExternalRecord(id) => id,
76            Output::Future(id, ..) => id,
77            Output::DynamicRecord(id) => id,
78            Output::RecordWithDynamicID(commitment, ..) => commitment,
79            Output::ExternalRecordWithDynamicID(id, ..) => id,
80        }
81    }
82
83    /// Returns the output as a transition leaf.
84    /// Note: RecordWithDynamicID uses leaf variant 3 (same as Record) with version 2.
85    /// Note: ExternalRecordWithDynamicID uses leaf variant 4 (same as ExternalRecord) with version 2.
86    pub fn to_transition_leaf(&self, index: u8) -> TransitionLeaf<N> {
87        match self {
88            // RecordWithDynamicID produces leaf with version 2, variant 3.
89            Output::RecordWithDynamicID(..) => TransitionLeaf::new_record_with_dynamic_id(index, *self.id()),
90            // ExternalRecordWithDynamicID produces leaf with version 2, variant 4.
91            Output::ExternalRecordWithDynamicID(..) => {
92                TransitionLeaf::new_external_record_with_dynamic_id(index, *self.id())
93            }
94            // All other variants use their serialization variant byte.
95            _ => TransitionLeaf::new(index, self.variant(), *self.id()),
96        }
97    }
98
99    /// Returns the commitment and record, if the output is a record.
100    #[allow(clippy::type_complexity)]
101    pub const fn record(&self) -> Option<(&Field<N>, &Record<N, Ciphertext<N>>)> {
102        match self {
103            Output::Record(commitment, _, Some(record), _)
104            | Output::RecordWithDynamicID(commitment, _, Some(record), _, _) => Some((commitment, record)),
105            _ => None,
106        }
107    }
108
109    /// Consumes `self` and returns the commitment and record, if the output is a record.
110    #[allow(clippy::type_complexity)]
111    pub fn into_record(self) -> Option<(Field<N>, Record<N, Ciphertext<N>>)> {
112        match self {
113            Output::Record(commitment, _, Some(record), _)
114            | Output::RecordWithDynamicID(commitment, _, Some(record), _, _) => Some((commitment, record)),
115            _ => None,
116        }
117    }
118
119    /// Returns the commitment, if the output is a record.
120    pub const fn commitment(&self) -> Option<&Field<N>> {
121        match self {
122            Output::Record(commitment, ..) | Output::RecordWithDynamicID(commitment, ..) => Some(commitment),
123            _ => None,
124        }
125    }
126
127    /// Returns the commitment, if the output is a record, and consumes `self`.
128    pub fn into_commitment(self) -> Option<Field<N>> {
129        match self {
130            Output::Record(commitment, ..) | Output::RecordWithDynamicID(commitment, ..) => Some(commitment),
131            _ => None,
132        }
133    }
134
135    /// Returns the nonce, if the output is a record.
136    pub const fn nonce(&self) -> Option<&Group<N>> {
137        match self {
138            Output::Record(_, _, Some(record), _) | Output::RecordWithDynamicID(_, _, Some(record), _, _) => {
139                Some(record.nonce())
140            }
141            _ => None,
142        }
143    }
144
145    /// Returns the nonce, if the output is a record, and consumes `self`.
146    pub fn into_nonce(self) -> Option<Group<N>> {
147        match self {
148            Output::Record(_, _, Some(record), _) | Output::RecordWithDynamicID(_, _, Some(record), _, _) => {
149                Some(record.into_nonce())
150            }
151            _ => None,
152        }
153    }
154
155    /// Returns the checksum, if the output is a record.
156    pub const fn checksum(&self) -> Option<&Field<N>> {
157        match self {
158            Output::Record(_, checksum, ..) | Output::RecordWithDynamicID(_, checksum, ..) => Some(checksum),
159            _ => None,
160        }
161    }
162
163    /// Returns the checksum, if the output is a record, and consumes `self`.
164    pub fn into_checksum(self) -> Option<Field<N>> {
165        match self {
166            Output::Record(_, checksum, ..) | Output::RecordWithDynamicID(_, checksum, ..) => Some(checksum),
167            _ => None,
168        }
169    }
170
171    /// Returns the sender ciphertext, if the output is a record.
172    pub const fn sender_ciphertext(&self) -> Option<&Field<N>> {
173        match self {
174            Output::Record(_, _, _, Some(sender_ciphertext))
175            | Output::RecordWithDynamicID(_, _, _, Some(sender_ciphertext), _) => Some(sender_ciphertext),
176            _ => None,
177        }
178    }
179
180    /// Returns the sender ciphertext, if the output is a record, and consumes `self`.
181    pub fn into_sender_ciphertext(self) -> Option<Field<N>> {
182        match self {
183            Output::Record(_, _, _, Some(sender_ciphertext))
184            | Output::RecordWithDynamicID(_, _, _, Some(sender_ciphertext), _) => Some(sender_ciphertext),
185            _ => None,
186        }
187    }
188
189    /// Returns the future, if the output is a future.
190    pub const fn future(&self) -> Option<&Future<N>> {
191        match self {
192            Output::Future(_, Some(future)) => Some(future),
193            _ => None,
194        }
195    }
196}
197
198impl<N: Network> Output<N> {
199    /// Returns the sender address, given the account view key of the record owner.
200    ///
201    /// If the output is not a record or does not contain a sender ciphertext, it returns `Ok(None)`.
202    /// If the record does not belong to the given account view key, it returns `Err`.
203    /// If the sender ciphertext is malformed or cannot be decrypted, it returns `Err`.
204    pub fn decrypt_sender_ciphertext(&self, account_view_key: &ViewKey<N>) -> Result<Option<Address<N>>> {
205        // Retrieve the record ciphertext and sender ciphertext, if they exist.
206        let (record_ciphertext, sender_ciphertext) = match self {
207            Output::Record(_, _, Some(record_ciphertext), Some(sender_ciphertext))
208            | Output::RecordWithDynamicID(_, _, Some(record_ciphertext), Some(sender_ciphertext), _) => {
209                (record_ciphertext, sender_ciphertext)
210            }
211            // If the output is not a record or does not contain a sender ciphertext, return `None`.
212            _ => return Ok(None),
213        };
214
215        // Compute the record view key.
216        let record_view_key = (*record_ciphertext.nonce() * **account_view_key).to_x_coordinate();
217        // Retrieve the record owner.
218        let expected_owner = match record_ciphertext.owner().is_public() {
219            true => record_ciphertext.owner().decrypt_with_randomizer(&[])?,
220            false => {
221                // Prepare the randomizer for the record owner.
222                let randomizers = N::hash_many_psd8(&[N::encryption_domain(), record_view_key], 1);
223                ensure!(randomizers.len() == 1, "Expected exactly one randomizer for the record owner");
224                // Decrypt the record owner using the randomizer.
225                record_ciphertext.owner().decrypt_with_randomizer(&[randomizers[0]])?
226            }
227        };
228        // Ensure this record belongs to the given account view key.
229        ensure!(
230            *expected_owner == account_view_key.to_address(),
231            "The record does not belong to the given account view key"
232        );
233
234        // Compute the encryption randomizer for the sender ciphertext.
235        let Ok(randomizer) = N::hash_psd4(&[N::encryption_domain(), record_view_key, Field::one()]) else {
236            bail!("Failed to compute the encryption randomizer for the sender ciphertext");
237        };
238        // Decrypt the sender ciphertext using the record view key.
239        let sender_x_coordinate = *sender_ciphertext - randomizer;
240        // Recover the sender address.
241        match Address::from_field(&sender_x_coordinate) {
242            Ok(sender_address) => Ok(Some(sender_address)),
243            Err(error) => bail!("Failed to recover the sender address - {error}"),
244        }
245    }
246}
247
248impl<N: Network> Output<N> {
249    /// Returns the public verifier inputs for the proof.
250    pub fn verifier_inputs(&self) -> impl '_ + Iterator<Item = N::Field> {
251        // Append the output ID.
252        [**self.id()].into_iter()
253            // Append the checksum and sender ciphertext, if they exist.
254            .chain([self.checksum().map(|sum| **sum), self.sender_ciphertext().map(|sender| **sender)].into_iter().flatten())
255    }
256
257    /// Returns the dynamic ID, if the output carries one.
258    pub const fn dynamic_id(&self) -> Option<&Field<N>> {
259        match self {
260            Output::RecordWithDynamicID(_, _, _, _, dynamic_id)
261            | Output::ExternalRecordWithDynamicID(_, dynamic_id) => Some(dynamic_id),
262            _ => None,
263        }
264    }
265
266    /// Returns the output from the caller's perspective.
267    pub fn to_caller_output(&self) -> Self {
268        match self {
269            // `RecordWithDynamicID` becomes `DynamicRecord` from caller's view.
270            Self::RecordWithDynamicID(_, _, _, _, dynamic_id) => Self::DynamicRecord(*dynamic_id),
271            // `ExternalRecordWithDynamicID` becomes `DynamicRecord` from caller's view.
272            Self::ExternalRecordWithDynamicID(_, dynamic_id) => Self::DynamicRecord(*dynamic_id),
273            // All other variants are unchanged.
274            other => other.clone(),
275        }
276    }
277
278    /// Returns `true` if the output is well-formed.
279    /// If the optional value exists, this method checks that it hashes to the output ID.
280    pub fn verify(&self, function_id: Field<N>, tcm: &Field<N>, index: usize) -> bool {
281        // Ensure the hash of the value (if the value exists) is correct.
282        let result = || match self {
283            Output::Constant(hash, Some(output)) => {
284                match output.to_fields() {
285                    Ok(fields) => {
286                        // Construct the (console) output index as a field element.
287                        let index = Field::from_u16(index as u16);
288                        // Construct the preimage as `(function ID || output || tcm || index)`.
289                        let mut preimage = Vec::new();
290                        preimage.push(function_id);
291                        preimage.extend(fields);
292                        preimage.push(*tcm);
293                        preimage.push(index);
294                        // Ensure the hash matches.
295                        match N::hash_psd8(&preimage) {
296                            Ok(candidate_hash) => Ok(hash == &candidate_hash),
297                            Err(error) => Err(error),
298                        }
299                    }
300                    Err(error) => Err(error),
301                }
302            }
303            Output::Public(hash, Some(output)) => {
304                match output.to_fields() {
305                    Ok(fields) => {
306                        // Construct the (console) output index as a field element.
307                        let index = Field::from_u16(index as u16);
308                        // Construct the preimage as `(function ID || output || tcm || index)`.
309                        let mut preimage = Vec::new();
310                        preimage.push(function_id);
311                        preimage.extend(fields);
312                        preimage.push(*tcm);
313                        preimage.push(index);
314                        // Ensure the hash matches.
315                        match N::hash_psd8(&preimage) {
316                            Ok(candidate_hash) => Ok(hash == &candidate_hash),
317                            Err(error) => Err(error),
318                        }
319                    }
320                    Err(error) => Err(error),
321                }
322            }
323            Output::Private(hash, Some(value)) => {
324                match value.to_fields() {
325                    // Ensure the hash matches.
326                    Ok(fields) => match N::hash_psd8(&fields) {
327                        Ok(candidate_hash) => Ok(hash == &candidate_hash),
328                        Err(error) => Err(error),
329                    },
330                    Err(error) => Err(error),
331                }
332            }
333            Output::Record(_, checksum, Some(record_ciphertext), sender_ciphertext)
334            | Output::RecordWithDynamicID(_, checksum, Some(record_ciphertext), sender_ciphertext, _) => {
335                // Construct the checksum preimage.
336                let mut preimage = record_ciphertext.to_bits_le();
337                // If the record version is set to Version 0, ensure the sender ciphertext is `None`.
338                // If the record version is set to Version 1 or higher, ensure the sender ciphertext is `Some` and non-zero.
339                if **record_ciphertext.version() == 0 {
340                    ensure!(sender_ciphertext.is_none(), "The sender ciphertext must be None for Version 0 records");
341                    // Truncate the last 8 bits of the preimage, as Version 0 records do not include the version in serialization.
342                    preimage.truncate(preimage.len().saturating_sub(8));
343                } else if **record_ciphertext.version() == 1 {
344                    ensure!(sender_ciphertext.is_some(), "The sender ciphertext must be non-empty");
345                    // Note: The sender ciphertext feature can become optional or deactivated by removing this check.
346                    // Safe: sender_ciphertext.is_some() was verified on the line above.
347                    ensure!(sender_ciphertext.unwrap() != Field::zero(), "The sender ciphertext must be non-zero");
348                } else {
349                    bail!(
350                        "The record version must be set to Version 0 or 1, but found Version {}",
351                        **record_ciphertext.version()
352                    );
353                }
354
355                // Ensure the record ciphertext hash matches the checksum.
356                match N::hash_bhp1024(&preimage) {
357                    Ok(candidate_hash) => Ok(checksum == &candidate_hash),
358                    Err(error) => Err(error),
359                }
360            }
361            Output::Future(hash, Some(output)) => {
362                match output.to_fields() {
363                    Ok(fields) => {
364                        // Construct the (future) output index as a field element.
365                        let index = Field::from_u16(index as u16);
366                        // Construct the preimage as `(function ID || output || tcm || index)`.
367                        let mut preimage = Vec::new();
368                        preimage.push(function_id);
369                        preimage.extend(fields);
370                        preimage.push(*tcm);
371                        preimage.push(index);
372                        // Ensure the hash matches.
373                        match N::hash_psd8(&preimage) {
374                            Ok(candidate_hash) => Ok(hash == &candidate_hash),
375                            Err(error) => Err(error),
376                        }
377                    }
378                    Err(error) => Err(error),
379                }
380            }
381            Output::Constant(_, None)
382            | Output::Public(_, None)
383            | Output::Private(_, None)
384            | Output::Record(_, _, None, _)
385            | Output::RecordWithDynamicID(_, _, None, _, _)
386            | Output::Future(_, None) => {
387                // This enforces that the transition *must* contain the value for this transition output.
388                // A similar rule is enforced for the transition input.
389                bail!("A transition output value is missing")
390            }
391            Output::ExternalRecord(_) => Ok(true),
392            Output::DynamicRecord(_) => Ok(true),
393            Output::ExternalRecordWithDynamicID(_, _) => Ok(true),
394        };
395
396        match result() {
397            Ok(is_hash_valid) => is_hash_valid,
398            Err(error) => {
399                eprintln!("{error}");
400                false
401            }
402        }
403    }
404
405    /// Returns `true` if the output matches the expected value type.
406    pub fn is_type(&self, expected_value_type: &ValueType<N>) -> bool {
407        matches!(
408            (self, expected_value_type),
409            (Self::Constant(..), ValueType::Constant(..))
410                | (Self::Public(..), ValueType::Public(..))
411                | (Self::Private(..), ValueType::Private(..))
412                | (Self::Record(..), ValueType::Record(..))
413                | (Self::RecordWithDynamicID(..), ValueType::Record(..))
414                | (Self::ExternalRecord(..), ValueType::ExternalRecord(..))
415                | (Self::ExternalRecordWithDynamicID(..), ValueType::ExternalRecord(..))
416                | (Self::Future(..), ValueType::Future(..))
417                | (Self::DynamicRecord(..), ValueType::DynamicRecord)
418        )
419    }
420}
421
422#[cfg(test)]
423pub(crate) mod test_helpers {
424    use super::*;
425    use console::{network::MainnetV0, program::Literal};
426
427    type CurrentNetwork = MainnetV0;
428
429    /// Sample the transition outputs.
430    pub(crate) fn sample_outputs() -> Vec<(<CurrentNetwork as Network>::TransitionID, Output<CurrentNetwork>)> {
431        let rng = &mut TestRng::default();
432
433        // Sample a transition.
434        let transaction = crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng, 0);
435        let transition = transaction.transitions().next().unwrap();
436
437        // Retrieve the transition ID and output.
438        let transition_id = *transition.id();
439        let input = transition.outputs().iter().next().unwrap().clone();
440
441        // Sample a random plaintext.
442        let plaintext = Plaintext::Literal(Literal::Field(Uniform::rand(rng)), Default::default());
443        let plaintext_hash = CurrentNetwork::hash_bhp1024(&plaintext.to_bits_le()).unwrap();
444        // Sample a random ciphertext.
445        let fields: Vec<_> = (0..10).map(|_| Uniform::rand(rng)).collect();
446        let ciphertext = Ciphertext::from_fields(&fields).unwrap();
447        let ciphertext_hash = CurrentNetwork::hash_bhp1024(&ciphertext.to_bits_le()).unwrap();
448        // Sample a random record.
449        let randomizer = Uniform::rand(rng);
450        let nonce = CurrentNetwork::g_scalar_multiply(&randomizer);
451        let record = Record::<CurrentNetwork, Plaintext<CurrentNetwork>>::from_str(
452            &format!("{{ owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.private, token_amount: 100u64.private, _nonce: {nonce}.public }}"),
453        ).unwrap();
454        let record_ciphertext = record.encrypt(randomizer).unwrap();
455        let record_checksum = CurrentNetwork::hash_bhp1024(&record_ciphertext.to_bits_le()).unwrap();
456        // Sample a sender ciphertext.
457        let sender_ciphertext = match record_ciphertext.version().is_zero() {
458            true => None,
459            false => Some(Uniform::rand(rng)),
460        };
461
462        vec![
463            (transition_id, input),
464            (Uniform::rand(rng), Output::Constant(Uniform::rand(rng), None)),
465            (Uniform::rand(rng), Output::Constant(plaintext_hash, Some(plaintext.clone()))),
466            (Uniform::rand(rng), Output::Public(Uniform::rand(rng), None)),
467            (Uniform::rand(rng), Output::Public(plaintext_hash, Some(plaintext))),
468            (Uniform::rand(rng), Output::Private(Uniform::rand(rng), None)),
469            (Uniform::rand(rng), Output::Private(ciphertext_hash, Some(ciphertext))),
470            (Uniform::rand(rng), Output::Record(Uniform::rand(rng), Uniform::rand(rng), None, sender_ciphertext)),
471            (
472                Uniform::rand(rng),
473                Output::Record(Uniform::rand(rng), record_checksum, Some(record_ciphertext.clone()), sender_ciphertext),
474            ),
475            (Uniform::rand(rng), Output::ExternalRecord(Uniform::rand(rng))),
476            (
477                Uniform::rand(rng),
478                Output::RecordWithDynamicID(
479                    Uniform::rand(rng),
480                    record_checksum,
481                    Some(record_ciphertext),
482                    sender_ciphertext,
483                    Uniform::rand(rng),
484                ),
485            ),
486            (
487                Uniform::rand(rng),
488                Output::RecordWithDynamicID(
489                    Uniform::rand(rng),
490                    Uniform::rand(rng),
491                    None,
492                    sender_ciphertext,
493                    Uniform::rand(rng),
494                ),
495            ),
496            (Uniform::rand(rng), Output::ExternalRecordWithDynamicID(Uniform::rand(rng), Uniform::rand(rng))),
497            (Uniform::rand(rng), Output::DynamicRecord(Uniform::rand(rng))),
498        ]
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use console::network::MainnetV0;
506
507    type CurrentNetwork = MainnetV0;
508
509    #[test]
510    fn test_to_caller_output_record_with_dynamic_id() {
511        // RecordWithDynamicID should become DynamicRecord(dynamic_id) from caller's view.
512        let commitment = Field::<CurrentNetwork>::from_u64(1);
513        let checksum = Field::<CurrentNetwork>::from_u64(2);
514        let dynamic_id = Field::<CurrentNetwork>::from_u64(3);
515
516        let output = Output::<CurrentNetwork>::RecordWithDynamicID(commitment, checksum, None, None, dynamic_id);
517        let caller_output = output.to_caller_output();
518
519        assert_eq!(caller_output, Output::<CurrentNetwork>::DynamicRecord(dynamic_id));
520    }
521
522    #[test]
523    fn test_to_caller_output_external_record_with_dynamic_id() {
524        // ExternalRecordWithDynamicID should become DynamicRecord(dynamic_id) from caller's view.
525        let ext_id = Field::<CurrentNetwork>::from_u64(10);
526        let dynamic_id = Field::<CurrentNetwork>::from_u64(20);
527
528        let output = Output::<CurrentNetwork>::ExternalRecordWithDynamicID(ext_id, dynamic_id);
529        let caller_output = output.to_caller_output();
530
531        assert_eq!(caller_output, Output::<CurrentNetwork>::DynamicRecord(dynamic_id));
532    }
533
534    #[test]
535    fn test_to_caller_output_non_dynamic_variants_unchanged() {
536        // Non-dynamic variants must be returned unchanged.
537        let id = Field::<CurrentNetwork>::from_u64(42);
538
539        let constant = Output::<CurrentNetwork>::Constant(id, None);
540        assert_eq!(constant.to_caller_output(), constant);
541
542        let dynamic_record = Output::<CurrentNetwork>::DynamicRecord(id);
543        assert_eq!(dynamic_record.to_caller_output(), dynamic_record);
544
545        let external = Output::<CurrentNetwork>::ExternalRecord(id);
546        assert_eq!(external.to_caller_output(), external);
547    }
548}