/ ledger / block / src / transition / mod.rs
mod.rs
  1  // Copyright (c) 2019-2025 Alpha-Delta Network Inc.
  2  // This file is part of the deltavm 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  
 16  pub mod input;
 17  pub use input::Input;
 18  
 19  pub mod output;
 20  pub use output::Output;
 21  
 22  mod bytes;
 23  mod merkle;
 24  mod serialize;
 25  mod string;
 26  
 27  use 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)]
 51  pub 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  
 70  impl<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_id:?}, {input}"),
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  
282  impl<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  
324  impl<N: Network> Transition<N> {
325      /// Returns `true` if this is a `credits.delta/*` transition.
326      #[inline]
327      pub fn is_credits(&self) -> bool {
328          self.program_id.to_string() == "credits.delta"
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.delta"
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.delta"
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.delta"
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.delta"
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.delta"
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.delta"
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.delta"
391              && self.function_name.to_string() == "upgrade"
392      }
393  }
394  
395  impl<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  
420  impl<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  
435  impl<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  
476  impl<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)]
518  pub 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, 0)
528          {
529              execution.into_transitions().next().unwrap()
530          } else {
531              unreachable!()
532          }
533      }
534  }