snarkvm_ledger_block/transition/output/
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
16mod bytes;
17mod serialize;
18mod string;
19
20use console::{
21    account::{Address, ViewKey},
22    network::prelude::*,
23    program::{Ciphertext, Future, Plaintext, Record, TransitionLeaf},
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}
45
46impl<N: Network> Output<N> {
47    /// Returns the variant of the output.
48    pub const fn variant(&self) -> Variant {
49        match self {
50            Output::Constant(_, _) => 0,
51            Output::Public(_, _) => 1,
52            Output::Private(_, _) => 2,
53            Output::Record(_, _, _, _) => 3,
54            Output::ExternalRecord(_) => 4,
55            Output::Future(_, _) => 5,
56        }
57    }
58
59    /// Returns the ID of the output.
60    pub const fn id(&self) -> &Field<N> {
61        match self {
62            Output::Constant(id, ..) => id,
63            Output::Public(id, ..) => id,
64            Output::Private(id, ..) => id,
65            Output::Record(commitment, ..) => commitment,
66            Output::ExternalRecord(id) => id,
67            Output::Future(id, ..) => id,
68        }
69    }
70
71    /// Returns the output as a transition leaf.
72    pub fn to_transition_leaf(&self, index: u8) -> TransitionLeaf<N> {
73        TransitionLeaf::new_with_version(index, self.variant(), *self.id())
74    }
75
76    /// Returns the commitment and record, if the output is a record.
77    #[allow(clippy::type_complexity)]
78    pub const fn record(&self) -> Option<(&Field<N>, &Record<N, Ciphertext<N>>)> {
79        match self {
80            Output::Record(commitment, _, Some(record), _) => Some((commitment, record)),
81            _ => None,
82        }
83    }
84
85    /// Consumes `self` and returns the commitment and record, if the output is a record.
86    #[allow(clippy::type_complexity)]
87    pub fn into_record(self) -> Option<(Field<N>, Record<N, Ciphertext<N>>)> {
88        match self {
89            Output::Record(commitment, _, Some(record), _) => Some((commitment, record)),
90            _ => None,
91        }
92    }
93
94    /// Returns the commitment, if the output is a record.
95    pub const fn commitment(&self) -> Option<&Field<N>> {
96        match self {
97            Output::Record(commitment, ..) => Some(commitment),
98            _ => None,
99        }
100    }
101
102    /// Returns the commitment, if the output is a record, and consumes `self`.
103    pub fn into_commitment(self) -> Option<Field<N>> {
104        match self {
105            Output::Record(commitment, ..) => Some(commitment),
106            _ => None,
107        }
108    }
109
110    /// Returns the nonce, if the output is a record.
111    pub const fn nonce(&self) -> Option<&Group<N>> {
112        match self {
113            Output::Record(_, _, Some(record), _) => Some(record.nonce()),
114            _ => None,
115        }
116    }
117
118    /// Returns the nonce, if the output is a record, and consumes `self`.
119    pub fn into_nonce(self) -> Option<Group<N>> {
120        match self {
121            Output::Record(_, _, Some(record), _) => Some(record.into_nonce()),
122            _ => None,
123        }
124    }
125
126    /// Returns the checksum, if the output is a record.
127    pub const fn checksum(&self) -> Option<&Field<N>> {
128        match self {
129            Output::Record(_, checksum, ..) => Some(checksum),
130            _ => None,
131        }
132    }
133
134    /// Returns the checksum, if the output is a record, and consumes `self`.
135    pub fn into_checksum(self) -> Option<Field<N>> {
136        match self {
137            Output::Record(_, checksum, ..) => Some(checksum),
138            _ => None,
139        }
140    }
141
142    /// Returns the sender ciphertext, if the output is a record.
143    pub const fn sender_ciphertext(&self) -> Option<&Field<N>> {
144        match self {
145            Output::Record(_, _, _, Some(sender_ciphertext)) => Some(sender_ciphertext),
146            _ => None,
147        }
148    }
149
150    /// Returns the sender ciphertext, if the output is a record, and consumes `self`.
151    pub fn into_sender_ciphertext(self) -> Option<Field<N>> {
152        match self {
153            Output::Record(_, _, _, Some(sender_ciphertext)) => Some(sender_ciphertext),
154            _ => None,
155        }
156    }
157
158    /// Returns the future, if the output is a future.
159    pub const fn future(&self) -> Option<&Future<N>> {
160        match self {
161            Output::Future(_, Some(future)) => Some(future),
162            _ => None,
163        }
164    }
165}
166
167impl<N: Network> Output<N> {
168    /// Returns the sender address, given the account view key of the record owner.
169    ///
170    /// If the output is not a record or does not contain a sender ciphertext, it returns `Ok(None)`.
171    /// If the record does not belong to the given account view key, it returns `Err`.
172    /// If the sender ciphertext is malformed or cannot be decrypted, it returns `Err`.
173    pub fn decrypt_sender_ciphertext(&self, account_view_key: &ViewKey<N>) -> Result<Option<Address<N>>> {
174        // Retrieve the record ciphertext and sender ciphertext, if they exist.
175        let (record_ciphertext, sender_ciphertext) = match self {
176            Output::Record(_, _, Some(record_ciphertext), Some(sender_ciphertext)) => {
177                (record_ciphertext, sender_ciphertext)
178            }
179            // If the output is not a record or does not contain a sender ciphertext, return `None`.
180            _ => return Ok(None),
181        };
182
183        // Compute the record view key.
184        let record_view_key = (*record_ciphertext.nonce() * **account_view_key).to_x_coordinate();
185        // Retrieve the record owner.
186        let expected_owner = match record_ciphertext.owner().is_public() {
187            true => record_ciphertext.owner().decrypt_with_randomizer(&[])?,
188            false => {
189                // Prepare the randomizer for the record owner.
190                let randomizers = N::hash_many_psd8(&[N::encryption_domain(), record_view_key], 1);
191                ensure!(randomizers.len() == 1, "Expected exactly one randomizer for the record owner");
192                // Decrypt the record owner using the randomizer.
193                record_ciphertext.owner().decrypt_with_randomizer(&[randomizers[0]])?
194            }
195        };
196        // Ensure this record belongs to the given account view key.
197        ensure!(
198            *expected_owner == account_view_key.to_address(),
199            "The record does not belong to the given account view key"
200        );
201
202        // Compute the encryption randomizer for the sender ciphertext.
203        let Ok(randomizer) = N::hash_psd4(&[N::encryption_domain(), record_view_key, Field::one()]) else {
204            bail!("Failed to compute the encryption randomizer for the sender ciphertext");
205        };
206        // Decrypt the sender ciphertext using the record view key.
207        let sender_x_coordinate = *sender_ciphertext - randomizer;
208        // Recover the sender address.
209        match Address::from_field(&sender_x_coordinate) {
210            Ok(sender_address) => Ok(Some(sender_address)),
211            Err(error) => bail!("Failed to recover the sender address - {error}"),
212        }
213    }
214}
215
216impl<N: Network> Output<N> {
217    /// Returns the public verifier inputs for the proof.
218    pub fn verifier_inputs(&self) -> impl '_ + Iterator<Item = N::Field> {
219        // Append the output ID.
220        [**self.id()].into_iter()
221            // Append the checksum and sender ciphertext, if they exist.
222            .chain([self.checksum().map(|sum| **sum), self.sender_ciphertext().map(|sender| **sender)].into_iter().flatten())
223    }
224
225    /// Returns `true` if the output is well-formed.
226    /// If the optional value exists, this method checks that it hashes to the output ID.
227    pub fn verify(&self, function_id: Field<N>, tcm: &Field<N>, index: usize) -> bool {
228        // Ensure the hash of the value (if the value exists) is correct.
229        let result = || match self {
230            Output::Constant(hash, Some(output)) => {
231                match output.to_fields() {
232                    Ok(fields) => {
233                        // Construct the (console) output index as a field element.
234                        let index = Field::from_u16(index as u16);
235                        // Construct the preimage as `(function ID || output || tcm || index)`.
236                        let mut preimage = Vec::new();
237                        preimage.push(function_id);
238                        preimage.extend(fields);
239                        preimage.push(*tcm);
240                        preimage.push(index);
241                        // Ensure the hash matches.
242                        match N::hash_psd8(&preimage) {
243                            Ok(candidate_hash) => Ok(hash == &candidate_hash),
244                            Err(error) => Err(error),
245                        }
246                    }
247                    Err(error) => Err(error),
248                }
249            }
250            Output::Public(hash, Some(output)) => {
251                match output.to_fields() {
252                    Ok(fields) => {
253                        // Construct the (console) output index as a field element.
254                        let index = Field::from_u16(index as u16);
255                        // Construct the preimage as `(function ID || output || tcm || index)`.
256                        let mut preimage = Vec::new();
257                        preimage.push(function_id);
258                        preimage.extend(fields);
259                        preimage.push(*tcm);
260                        preimage.push(index);
261                        // Ensure the hash matches.
262                        match N::hash_psd8(&preimage) {
263                            Ok(candidate_hash) => Ok(hash == &candidate_hash),
264                            Err(error) => Err(error),
265                        }
266                    }
267                    Err(error) => Err(error),
268                }
269            }
270            Output::Private(hash, Some(value)) => {
271                match value.to_fields() {
272                    // Ensure the hash matches.
273                    Ok(fields) => match N::hash_psd8(&fields) {
274                        Ok(candidate_hash) => Ok(hash == &candidate_hash),
275                        Err(error) => Err(error),
276                    },
277                    Err(error) => Err(error),
278                }
279            }
280            Output::Record(_, checksum, Some(record_ciphertext), sender_ciphertext) => {
281                // Construct the checksum preimage.
282                let mut preimage = record_ciphertext.to_bits_le();
283                // If the record version is set to Version 0, ensure the sender ciphertext is `None`.
284                // If the record version is set to Version 1 or higher, ensure the sender ciphertext is `Some` and non-zero.
285                if **record_ciphertext.version() == 0 {
286                    ensure!(sender_ciphertext.is_none(), "The sender ciphertext must be None for Version 0 records");
287                    // Truncate the last 8 bits of the preimage, as Version 0 records do not include the version in serialization.
288                    preimage.truncate(preimage.len().saturating_sub(8));
289                } else if **record_ciphertext.version() == 1 {
290                    ensure!(sender_ciphertext.is_some(), "The sender ciphertext must be non-empty");
291                    // Note: The sender ciphertext feature can become optional or deactivated by removing this check.
292                    ensure!(sender_ciphertext.unwrap() != Field::zero(), "The sender ciphertext must be non-zero");
293                } else {
294                    bail!(
295                        "The record version must be set to Version 0 or 1, but found Version {}",
296                        **record_ciphertext.version()
297                    );
298                }
299
300                // Ensure the record ciphertext hash matches the checksum.
301                match N::hash_bhp1024(&preimage) {
302                    Ok(candidate_hash) => Ok(checksum == &candidate_hash),
303                    Err(error) => Err(error),
304                }
305            }
306            Output::Future(hash, Some(output)) => {
307                match output.to_fields() {
308                    Ok(fields) => {
309                        // Construct the (future) output index as a field element.
310                        let index = Field::from_u16(index as u16);
311                        // Construct the preimage as `(function ID || output || tcm || index)`.
312                        let mut preimage = Vec::new();
313                        preimage.push(function_id);
314                        preimage.extend(fields);
315                        preimage.push(*tcm);
316                        preimage.push(index);
317                        // Ensure the hash matches.
318                        match N::hash_psd8(&preimage) {
319                            Ok(candidate_hash) => Ok(hash == &candidate_hash),
320                            Err(error) => Err(error),
321                        }
322                    }
323                    Err(error) => Err(error),
324                }
325            }
326            Output::Constant(_, None)
327            | Output::Public(_, None)
328            | Output::Private(_, None)
329            | Output::Record(_, _, None, _)
330            | Output::Future(_, None) => {
331                // This enforces that the transition *must* contain the value for this transition output.
332                // A similar rule is enforced for the transition input.
333                bail!("A transition output value is missing")
334            }
335            Output::ExternalRecord(_) => Ok(true),
336        };
337
338        match result() {
339            Ok(is_hash_valid) => is_hash_valid,
340            Err(error) => {
341                eprintln!("{error}");
342                false
343            }
344        }
345    }
346}
347
348#[cfg(test)]
349pub(crate) mod test_helpers {
350    use super::*;
351    use console::{network::MainnetV0, program::Literal};
352
353    type CurrentNetwork = MainnetV0;
354
355    /// Sample the transition outputs.
356    pub(crate) fn sample_outputs() -> Vec<(<CurrentNetwork as Network>::TransitionID, Output<CurrentNetwork>)> {
357        let rng = &mut TestRng::default();
358
359        // Sample a transition.
360        let transaction = crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng, 0);
361        let transition = transaction.transitions().next().unwrap();
362
363        // Retrieve the transition ID and input.
364        let transition_id = *transition.id();
365        let input = transition.outputs().iter().next().unwrap().clone();
366
367        // Sample a random plaintext.
368        let plaintext = Plaintext::Literal(Literal::Field(Uniform::rand(rng)), Default::default());
369        let plaintext_hash = CurrentNetwork::hash_bhp1024(&plaintext.to_bits_le()).unwrap();
370        // Sample a random ciphertext.
371        let fields: Vec<_> = (0..10).map(|_| Uniform::rand(rng)).collect();
372        let ciphertext = Ciphertext::from_fields(&fields).unwrap();
373        let ciphertext_hash = CurrentNetwork::hash_bhp1024(&ciphertext.to_bits_le()).unwrap();
374        // Sample a random record.
375        let randomizer = Uniform::rand(rng);
376        let nonce = CurrentNetwork::g_scalar_multiply(&randomizer);
377        let record = Record::<CurrentNetwork, Plaintext<CurrentNetwork>>::from_str(
378            &format!("{{ owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.private, token_amount: 100u64.private, _nonce: {nonce}.public }}"),
379        ).unwrap();
380        let record_ciphertext = record.encrypt(randomizer).unwrap();
381        let record_checksum = CurrentNetwork::hash_bhp1024(&record_ciphertext.to_bits_le()).unwrap();
382        // Sample a sender ciphertext.
383        let sender_ciphertext = match record_ciphertext.version().is_zero() {
384            true => None,
385            false => Some(Uniform::rand(rng)),
386        };
387
388        vec![
389            (transition_id, input),
390            (Uniform::rand(rng), Output::Constant(Uniform::rand(rng), None)),
391            (Uniform::rand(rng), Output::Constant(plaintext_hash, Some(plaintext.clone()))),
392            (Uniform::rand(rng), Output::Public(Uniform::rand(rng), None)),
393            (Uniform::rand(rng), Output::Public(plaintext_hash, Some(plaintext))),
394            (Uniform::rand(rng), Output::Private(Uniform::rand(rng), None)),
395            (Uniform::rand(rng), Output::Private(ciphertext_hash, Some(ciphertext))),
396            (Uniform::rand(rng), Output::Record(Uniform::rand(rng), Uniform::rand(rng), None, sender_ciphertext)),
397            (
398                Uniform::rand(rng),
399                Output::Record(Uniform::rand(rng), record_checksum, Some(record_ciphertext), sender_ciphertext),
400            ),
401            (Uniform::rand(rng), Output::ExternalRecord(Uniform::rand(rng))),
402        ]
403    }
404}