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 }