verify.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 use super::*; 17 18 impl<A: Alpha> Request<A> { 19 /// Returns `true` if the input IDs are derived correctly, the input records all belong to the signer, 20 /// and the signature is valid. 21 /// 22 /// Verifies (challenge == challenge') && (address == address') && (serial_numbers == serial_numbers') where: 23 /// challenge' := HashToScalar(r * G, pk_sig, pr_sig, signer, \[tvk, tcm, function ID, is_root, program checksum?, input IDs\]) 24 /// The program checksum must be provided if the program has a constructor and should not be provided otherwise. 25 pub fn verify( 26 &self, 27 input_types: &[console::ValueType<A::Network>], 28 tpk: &Group<A>, 29 root_tvk: Option<Field<A>>, 30 is_root: Boolean<A>, 31 program_checksum: Option<Field<A>>, 32 ) -> Boolean<A> { 33 // Compute the function ID. 34 let function_id = compute_function_id(&self.network_id, &self.program_id, &self.function_name); 35 36 // Compute 'is_root' as a field element. 37 let is_root = Ternary::ternary(&is_root, &Field::<A>::one(), &Field::<A>::zero()); 38 39 // Construct the signature message as `[tvk, tcm, function ID, input IDs]`. 40 let mut message = Vec::with_capacity(3 + 4 * self.input_ids.len()); 41 message.push(self.tvk.clone()); 42 message.push(self.tcm.clone()); 43 message.push(function_id); 44 message.push(is_root); 45 // Add the program checksum to the signature message if it was provided. 46 if let Some(program_checksum) = program_checksum { 47 message.push(program_checksum); 48 } 49 50 // Check the input IDs and construct the rest of the signature message. 51 let (input_checks, append_to_message) = Self::check_input_ids::<true>( 52 &self.network_id, 53 &self.program_id, 54 &self.function_name, 55 &self.input_ids, 56 &self.inputs, 57 input_types, 58 &self.signer, 59 &self.sk_tag, 60 &self.tvk, 61 &self.tcm, 62 Some(&self.signature), 63 ); 64 // Append the input elements to the message. 65 match append_to_message { 66 Some(append_to_message) => message.extend(append_to_message), 67 None => A::halt("Missing input elements in request verification"), 68 } 69 70 // Determine the root transition view key. 71 let root_tvk = root_tvk.unwrap_or(Field::<A>::new(Mode::Private, self.tvk.eject_value())); 72 73 // Verify the transition public key and commitments are well-formed. 74 let tpk_checks = { 75 // Compute the transition commitment as `Hash(tvk)`. 76 let tcm = A::hash_psd2(&[self.tvk.clone()]); 77 // Compute the signer commitment as `Hash(signer || root_tvk)`. 78 let scm = A::hash_psd2(&[self.signer.to_field(), root_tvk]); 79 80 // Ensure the transition public key matches with the saved one from the signature. 81 tpk.is_equal(&self.to_tpk()) 82 // Ensure the computed transition commitment matches. 83 & tcm.is_equal(&self.tcm) 84 // Ensure the computed signer commitment matches. 85 & scm.is_equal(&self.scm) 86 }; 87 88 // Verify the signature. 89 // Note: We copy/paste the Alpha signature verification code here in order to compute `tpk` only once. 90 let signature_checks = { 91 // Retrieve pk_sig. 92 let pk_sig = self.signature.compute_key().pk_sig(); 93 // Retrieve pr_sig. 94 let pr_sig = self.signature.compute_key().pr_sig(); 95 96 // Construct the hash input as (r * G, pk_sig, pr_sig, address, message). 97 let mut preimage = Vec::with_capacity(4 + message.len()); 98 preimage.extend([tpk, pk_sig, pr_sig].map(|point| point.to_x_coordinate())); 99 preimage.push(self.signer.to_field()); 100 preimage.extend_from_slice(&message); 101 102 // Compute the candidate verifier challenge. 103 let candidate_challenge = A::hash_to_scalar_psd8(&preimage); 104 // Compute the candidate address. 105 let candidate_address = self.signature.compute_key().to_address(); 106 107 // Return `true` if the challenge and address is valid. 108 self.signature.challenge().is_equal(&candidate_challenge) & self.signer.is_equal(&candidate_address) 109 }; 110 111 // Verify the signature, inputs, and `tpk` are valid. 112 signature_checks & input_checks & tpk_checks 113 } 114 115 /// Returns `true` if the inputs match their input IDs. 116 /// Note: This method does **not** perform signature checks. 117 pub fn check_input_ids<const CREATE_MESSAGE: bool>( 118 network_id: &U16<A>, 119 program_id: &ProgramID<A>, 120 function_name: &Identifier<A>, 121 input_ids: &[InputID<A>], 122 inputs: &[Value<A>], 123 input_types: &[console::ValueType<A::Network>], 124 signer: &Address<A>, 125 sk_tag: &Field<A>, 126 tvk: &Field<A>, 127 tcm: &Field<A>, 128 signature: Option<&Signature<A>>, 129 ) -> (Boolean<A>, Option<Vec<Field<A>>>) { 130 // Ensure the signature response matches the `CREATE_MESSAGE` flag. 131 match CREATE_MESSAGE { 132 true => assert!(signature.is_some()), 133 false => assert!(signature.is_none()), 134 } 135 136 // Compute the function ID. 137 let function_id = compute_function_id(network_id, program_id, function_name); 138 139 // Initialize a vector for a message. 140 let mut message = Vec::new(); 141 142 // Perform the input ID checks. 143 let input_checks = input_ids 144 .iter() 145 .zip_eq(inputs) 146 .zip_eq(input_types) 147 .enumerate() 148 .map(|(index, ((input_id, input), input_type))| { 149 match input_id { 150 // A constant input is hashed (using `tcm`) to a field element. 151 InputID::Constant(input_hash) => { 152 // Add the input hash to the message. 153 if CREATE_MESSAGE { 154 message.push(input_hash.clone()); 155 } 156 157 // Prepare the index as a constant field element. 158 let input_index = Field::constant(console::Field::from_u16(index as u16)); 159 // Construct the preimage as `(function ID || input || tcm || index)`. 160 let mut preimage = Vec::new(); 161 preimage.push(function_id.clone()); 162 preimage.extend(input.to_fields()); 163 preimage.push(tcm.clone()); 164 preimage.push(input_index); 165 166 // Ensure the expected hash matches the computed hash. 167 match &input { 168 Value::Plaintext(..) => input_hash.is_equal(&A::hash_psd8(&preimage)), 169 // Ensure the input is not a record or future. 170 Value::Record(..) => A::halt("Expected a constant plaintext input, found a record input"), 171 Value::Future(..) => A::halt("Expected a constant plaintext input, found a future input"), 172 } 173 } 174 // A public input is hashed (using `tcm`) to a field element. 175 InputID::Public(input_hash) => { 176 // Add the input hash to the message. 177 if CREATE_MESSAGE { 178 message.push(input_hash.clone()); 179 } 180 181 // Prepare the index as a constant field element. 182 let input_index = Field::constant(console::Field::from_u16(index as u16)); 183 // Construct the preimage as `(function ID || input || tcm || index)`. 184 let mut preimage = Vec::new(); 185 preimage.push(function_id.clone()); 186 preimage.extend(input.to_fields()); 187 preimage.push(tcm.clone()); 188 preimage.push(input_index); 189 190 // Ensure the expected hash matches the computed hash. 191 match &input { 192 Value::Plaintext(..) => input_hash.is_equal(&A::hash_psd8(&preimage)), 193 // Ensure the input is not a record or future. 194 Value::Record(..) => A::halt("Expected a public plaintext input, found a record input"), 195 Value::Future(..) => A::halt("Expected a public plaintext input, found a future input"), 196 } 197 } 198 // A private input is encrypted (using `tvk`) and hashed to a field element. 199 InputID::Private(input_hash) => { 200 // Add the input hash to the message. 201 if CREATE_MESSAGE { 202 message.push(input_hash.clone()); 203 } 204 205 // Prepare the index as a constant field element. 206 let input_index = Field::constant(console::Field::from_u16(index as u16)); 207 // Compute the input view key as `Hash(function ID || tvk || index)`. 208 let input_view_key = A::hash_psd4(&[function_id.clone(), tvk.clone(), input_index]); 209 // Compute the ciphertext. 210 let ciphertext = match &input { 211 Value::Plaintext(plaintext) => plaintext.encrypt_symmetric(input_view_key), 212 // Ensure the input is a plaintext. 213 Value::Record(..) => A::halt("Expected a private plaintext input, found a record input"), 214 Value::Future(..) => A::halt("Expected a private plaintext input, found a future input"), 215 }; 216 217 // Ensure the expected hash matches the computed hash. 218 input_hash.is_equal(&A::hash_psd8(&ciphertext.to_fields())) 219 } 220 // A record input is computed to its serial number. 221 InputID::Record(commitment, gamma, record_view_key, serial_number, tag) => { 222 // Retrieve the record. 223 let record = match &input { 224 Value::Record(record) => record, 225 // Ensure the input is a record. 226 Value::Plaintext(..) => A::halt("Expected a record input, found a plaintext input"), 227 Value::Future(..) => A::halt("Expected a record input, found a future input"), 228 }; 229 // Retrieve the record name as a `Mode::Constant`. 230 let record_name = match input_type { 231 console::ValueType::Record(record_name) => Identifier::constant(*record_name), 232 // Ensure the input is a record. 233 _ => A::halt(format!("Expected a record input at input {index}")), 234 }; 235 // Compute the record commitment. 236 let candidate_commitment = record.to_commitment(program_id, &record_name, record_view_key); 237 // Compute the `candidate_serial_number` from `gamma`. 238 let candidate_serial_number = 239 Record::<A, Plaintext<A>>::serial_number_from_gamma(gamma, candidate_commitment.clone()); 240 // Compute the tag. 241 let candidate_tag = 242 Record::<A, Plaintext<A>>::tag(sk_tag.clone(), candidate_commitment.clone()); 243 244 if CREATE_MESSAGE { 245 // Ensure the signature is declared. 246 let signature = match signature { 247 Some(signature) => signature, 248 None => A::halt("Missing signature in logic to check input IDs"), 249 }; 250 // Retrieve the challenge from the signature. 251 let challenge = signature.challenge(); 252 // Retrieve the response from the signature. 253 let response = signature.response(); 254 255 // Compute the generator `H` as `HashToGroup(commitment)`. 256 let h = A::hash_to_group_psd2(&[A::serial_number_domain(), candidate_commitment.clone()]); 257 // Compute `h_r` as `(challenge * gamma) + (response * H)`, equivalent to `r * H`. 258 let h_r = (gamma.deref() * challenge) + (&h * response); 259 260 // Add (`H`, `r * H`, `gamma`, `tag`) to the message. 261 message.extend([h, h_r, *gamma.clone()].iter().map(|point| point.to_x_coordinate())); 262 message.push(candidate_tag.clone()); 263 } 264 265 // Ensure the candidate serial number matches the expected serial number. 266 serial_number.is_equal(&candidate_serial_number) 267 // Ensure the candidate commitment matches the expected commitment. 268 & commitment.is_equal(&candidate_commitment) 269 // Ensure the candidate tag matches the expected tag. 270 & tag.is_equal(&candidate_tag) 271 // Ensure the record belongs to the signer. 272 & record.owner().deref().is_equal(signer) 273 } 274 // An external record input is hashed (using `tvk`) to a field element. 275 InputID::ExternalRecord(input_hash) => { 276 // Add the input hash to the message. 277 if CREATE_MESSAGE { 278 message.push(input_hash.clone()); 279 } 280 281 // Retrieve the record. 282 let record = match &input { 283 Value::Record(record) => record, 284 // Ensure the input is a record. 285 Value::Plaintext(..) => { 286 A::halt("Expected an external record input, found a plaintext input") 287 } 288 Value::Future(..) => A::halt("Expected an external record input, found a future input"), 289 }; 290 291 // Prepare the index as a constant field element. 292 let input_index = Field::constant(console::Field::from_u16(index as u16)); 293 // Construct the preimage as `(function ID || input || tvk || index)`. 294 let mut preimage = Vec::new(); 295 preimage.push(function_id.clone()); 296 preimage.extend(record.to_fields()); 297 preimage.push(tvk.clone()); 298 preimage.push(input_index); 299 300 // Ensure the expected hash matches the computed hash. 301 input_hash.is_equal(&A::hash_psd8(&preimage)) 302 } 303 } 304 }) 305 .fold(Boolean::constant(true), |acc, x| acc & x); 306 307 // Return the boolean, and (optional) the message. 308 match CREATE_MESSAGE { 309 true => (input_checks, Some(message)), 310 false => match message.is_empty() { 311 true => (input_checks, None), 312 false => A::halt("Malformed synthesis of the logic to check input IDs"), 313 }, 314 } 315 } 316 } 317 318 #[cfg(test)] 319 mod tests { 320 use super::*; 321 use crate::Circuit; 322 use deltavm_utilities::TestRng; 323 324 use anyhow::Result; 325 326 pub(crate) const ITERATIONS: usize = 50; 327 328 fn check_verify( 329 mode: Mode, 330 num_constants: u64, 331 num_public: u64, 332 num_private: u64, 333 num_constraints: u64, 334 set_program_checksum: bool, 335 ) -> Result<()> { 336 let rng = &mut TestRng::default(); 337 338 for i in 0..ITERATIONS { 339 // Sample a random private key and address. 340 let private_key = deltavm_console_account::PrivateKey::new(rng)?; 341 let address = deltavm_console_account::Address::try_from(&private_key).unwrap(); 342 343 // Construct a program ID and function name. 344 let program_id = console::ProgramID::from_str("token.delta")?; 345 let function_name = console::Identifier::from_str("transfer")?; 346 347 // Prepare a record belonging to the address. 348 let record_string = format!( 349 "{{ owner: {address}.private, token_amount: 100u64.private, _nonce: 0group.public, _version: 1u8.public }}" 350 ); 351 352 // Construct the inputs. 353 let input_constant = 354 console::Value::<<Circuit as Environment>::Network>::from_str("{ token_amount: 9876543210u128 }") 355 .unwrap(); 356 let input_public = 357 console::Value::<<Circuit as Environment>::Network>::from_str("{ token_amount: 9876543210u128 }") 358 .unwrap(); 359 let input_private = 360 console::Value::<<Circuit as Environment>::Network>::from_str("{ token_amount: 9876543210u128 }") 361 .unwrap(); 362 let input_record = console::Value::<<Circuit as Environment>::Network>::from_str(&record_string).unwrap(); 363 let input_external_record = 364 console::Value::<<Circuit as Environment>::Network>::from_str(&record_string).unwrap(); 365 let inputs = [input_constant, input_public, input_private, input_record, input_external_record]; 366 367 // Construct the input types. 368 let input_types = vec![ 369 console::ValueType::from_str("amount.constant").unwrap(), 370 console::ValueType::from_str("amount.public").unwrap(), 371 console::ValueType::from_str("amount.private").unwrap(), 372 console::ValueType::from_str("token.record").unwrap(), 373 console::ValueType::from_str("token.delta/token.record").unwrap(), 374 ]; 375 376 // Sample 'root_tvk'. 377 let root_tvk = None; 378 // Sample 'is_root'. 379 let is_root = true; 380 // Sample 'program_checksum'. 381 let program_checksum = set_program_checksum.then(|| console::Field::from_u64(i as u64)); 382 383 // Compute the signed request. 384 let request = console::Request::sign( 385 &private_key, 386 program_id, 387 function_name, 388 inputs.iter(), 389 &input_types, 390 root_tvk, 391 is_root, 392 program_checksum, 393 rng, 394 )?; 395 assert!(request.verify(&input_types, is_root, program_checksum)); 396 397 // Inject the request into a circuit. 398 let tpk = Group::<Circuit>::new(mode, request.to_tpk()); 399 let request = Request::<Circuit>::new(mode, request); 400 let is_root = Boolean::new(mode, is_root); 401 let program_checksum = program_checksum.map(|hash| Field::<Circuit>::new(mode, hash)); 402 403 Circuit::scope(format!("Request {i}"), || { 404 let root_tvk = None; 405 let candidate = request.verify(&input_types, &tpk, root_tvk, is_root, program_checksum); 406 assert!(candidate.eject_value()); 407 match mode.is_constant() { 408 true => assert_scope!(<=num_constants, <=num_public, <=num_private, <=num_constraints), 409 false => assert_scope!(<=num_constants, num_public, num_private, num_constraints), 410 } 411 }); 412 413 Circuit::scope(format!("Request {i}"), || { 414 let (candidate, _) = Request::check_input_ids::<false>( 415 request.network_id(), 416 request.program_id(), 417 request.function_name(), 418 request.input_ids(), 419 request.inputs(), 420 &input_types, 421 request.signer(), 422 request.sk_tag(), 423 request.tvk(), 424 request.tcm(), 425 None, 426 ); 427 assert!(candidate.eject_value()); 428 }); 429 Circuit::reset(); 430 } 431 Ok(()) 432 } 433 434 #[test] 435 fn test_sign_and_verify_constant() -> Result<()> { 436 // Note: This is correct. At this (high) level of a program, we override the default mode in the `Record` case, 437 // based on the user-defined visibility in the record type. Thus, we have nonzero private and constraint values. 438 // These bounds are determined experimentally. 439 check_verify(Mode::Constant, 43440, 0, 21629, 21656, false)?; 440 check_verify(Mode::Constant, 43440, 0, 21629, 21656, true) 441 } 442 443 #[test] 444 fn test_sign_and_verify_public() -> Result<()> { 445 check_verify(Mode::Public, 40938, 0, 30031, 30062, false)?; 446 check_verify(Mode::Public, 40938, 0, 30546, 30577, true) 447 } 448 449 #[test] 450 fn test_sign_and_verify_private() -> Result<()> { 451 check_verify(Mode::Private, 40938, 0, 30031, 30062, false)?; 452 check_verify(Mode::Private, 40938, 0, 30546, 30577, true) 453 } 454 }