verify.rs
1 // Copyright (c) 2019-2025 Alpha-Delta Network Inc. 2 // This file is part of the alphavm 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<N: Network> Request<N> { 19 /// Returns `true` if the request is valid, and `false` otherwise. 20 /// 21 /// Verifies (challenge == challenge') && (address == address') && (serial_numbers == serial_numbers') where: 22 /// challenge' := HashToScalar(r * G, pk_sig, pr_sig, signer, \[tvk, tcm, function ID, is_root, program checksum?, input IDs\]) 23 /// The program checksum must be provided if the program has a constructor and should not be provided otherwise. 24 pub fn verify(&self, input_types: &[ValueType<N>], is_root: bool, program_checksum: Option<Field<N>>) -> bool { 25 // Verify the transition public key, transition view key, and transition commitment are well-formed. 26 { 27 // Compute the transition commitment `tcm` as `Hash(tvk)`. 28 match N::hash_psd2(&[self.tvk]) { 29 Ok(tcm) => { 30 // Ensure the computed transition commitment matches. 31 if tcm != self.tcm { 32 eprintln!("Invalid transition commitment in request."); 33 return false; 34 } 35 } 36 Err(error) => { 37 eprintln!("Failed to compute transition commitment in request verification: {error}"); 38 return false; 39 } 40 } 41 } 42 43 // Retrieve the challenge from the signature. 44 let challenge = self.signature.challenge(); 45 // Retrieve the response from the signature. 46 let response = self.signature.response(); 47 48 // Compute the function ID. 49 let function_id = match compute_function_id(&self.network_id, &self.program_id, &self.function_name) { 50 Ok(function_id) => function_id, 51 Err(error) => { 52 eprintln!("Failed to construct the function ID: {error}"); 53 return false; 54 } 55 }; 56 57 // Compute the 'is_root' field. 58 let is_root = if is_root { Field::<N>::one() } else { Field::<N>::zero() }; 59 60 // Construct the signature message as `[tvk, tcm, function ID, input IDs]`. 61 let mut message = Vec::with_capacity(3 + self.input_ids.len()); 62 message.push(self.tvk); 63 message.push(self.tcm); 64 message.push(function_id); 65 message.push(is_root); 66 // Add the program checksum to the signature message if it was provided. 67 if let Some(program_checksum) = program_checksum { 68 message.push(program_checksum); 69 } 70 71 if let Err(error) = self.input_ids.iter().zip_eq(&self.inputs).zip_eq(input_types).enumerate().try_for_each( 72 |(index, ((input_id, input), input_type))| { 73 match input_id { 74 // A constant input is hashed (using `tcm`) to a field element. 75 InputID::Constant(input_hash) => { 76 // Ensure the input is a plaintext. 77 ensure!(matches!(input, Value::Plaintext(..)), "Expected a plaintext input"); 78 79 // Construct the (console) input index as a field element. 80 let index = Field::from_u16(u16::try_from(index).or_halt_with::<N>("Input index exceeds u16")); 81 // Construct the preimage as `(function ID || input || tcm || index)`. 82 let mut preimage = Vec::new(); 83 preimage.push(function_id); 84 preimage.extend(input.to_fields()?); 85 preimage.push(self.tcm); 86 preimage.push(index); 87 // Hash the input to a field element. 88 let candidate_hash = N::hash_psd8(&preimage)?; 89 // Ensure the input hash matches. 90 ensure!(*input_hash == candidate_hash, "Expected a constant input with the same hash"); 91 92 // Add the input hash to the message. 93 message.push(candidate_hash); 94 } 95 // A public input is hashed (using `tcm`) to a field element. 96 InputID::Public(input_hash) => { 97 // Ensure the input is a plaintext. 98 ensure!(matches!(input, Value::Plaintext(..)), "Expected a plaintext input"); 99 100 // Construct the (console) input index as a field element. 101 let index = Field::from_u16(u16::try_from(index).or_halt_with::<N>("Input index exceeds u16")); 102 // Construct the preimage as `(function ID || input || tcm || index)`. 103 let mut preimage = Vec::new(); 104 preimage.push(function_id); 105 preimage.extend(input.to_fields()?); 106 preimage.push(self.tcm); 107 preimage.push(index); 108 // Hash the input to a field element. 109 let candidate_hash = N::hash_psd8(&preimage)?; 110 // Ensure the input hash matches. 111 ensure!(*input_hash == candidate_hash, "Expected a public input with the same hash"); 112 113 // Add the input hash to the message. 114 message.push(candidate_hash); 115 } 116 // A private input is encrypted (using `tvk`) and hashed to a field element. 117 InputID::Private(input_hash) => { 118 // Ensure the input is a plaintext. 119 ensure!(matches!(input, Value::Plaintext(..)), "Expected a plaintext input"); 120 121 // Construct the (console) input index as a field element. 122 let index = Field::from_u16(u16::try_from(index).or_halt_with::<N>("Input index exceeds u16")); 123 // Compute the input view key as `Hash(function ID || tvk || index)`. 124 let input_view_key = N::hash_psd4(&[function_id, self.tvk, index])?; 125 // Compute the ciphertext. 126 let ciphertext = match &input { 127 Value::Plaintext(plaintext) => plaintext.encrypt_symmetric(input_view_key)?, 128 // Ensure the input is a plaintext. 129 Value::Record(..) => bail!("Expected a plaintext input, found a record input"), 130 Value::Future(..) => bail!("Expected a plaintext input, found a future input"), 131 }; 132 // Hash the ciphertext to a field element. 133 let candidate_hash = N::hash_psd8(&ciphertext.to_fields()?)?; 134 // Ensure the input hash matches. 135 ensure!(*input_hash == candidate_hash, "Expected a private input with the same hash"); 136 137 // Add the input hash to the message. 138 message.push(candidate_hash); 139 } 140 // A record input is computed to its serial number. 141 InputID::Record(commitment, gamma, record_view_key, serial_number, tag) => { 142 // Retrieve the record. 143 let record = match &input { 144 Value::Record(record) => record, 145 // Ensure the input is a record. 146 Value::Plaintext(..) => bail!("Expected a record input, found a plaintext input"), 147 Value::Future(..) => bail!("Expected a record input, found a future input"), 148 }; 149 // Retrieve the record name. 150 let record_name = match input_type { 151 ValueType::Record(record_name) => record_name, 152 // Ensure the input type is a record. 153 _ => bail!("Expected a record type at input {index}"), 154 }; 155 // Ensure the record belongs to the signer. 156 ensure!(**record.owner() == self.signer, "Input record does not belong to the signer"); 157 158 // Compute the record commitment. 159 let candidate_commitment = 160 record.to_commitment(&self.program_id, record_name, record_view_key)?; 161 // Ensure the commitment matches. 162 ensure!( 163 *commitment == candidate_commitment, 164 "Expected a record input with the same commitment" 165 ); 166 167 // Compute the `candidate_sn` from `gamma`. 168 let candidate_sn = Record::<N, Plaintext<N>>::serial_number_from_gamma(gamma, *commitment)?; 169 // Ensure the serial number matches. 170 ensure!(*serial_number == candidate_sn, "Expected a record input with the same serial number"); 171 172 // Compute the generator `H` as `HashToGroup(commitment)`. 173 let h = N::hash_to_group_psd2(&[N::serial_number_domain(), *commitment])?; 174 // Compute `h_r` as `(challenge * gamma) + (response * H)`, equivalent to `r * H`. 175 let h_r = (*gamma * challenge) + (h * response); 176 177 // Compute the tag as `Hash(sk_tag || commitment)`. 178 let candidate_tag = N::hash_psd2(&[self.sk_tag, *commitment])?; 179 // Ensure the tag matches. 180 ensure!(*tag == candidate_tag, "Expected a record input with the same tag"); 181 182 // Add (`H`, `r * H`, `gamma`, `tag`) to the message. 183 message.extend([h, h_r, *gamma].iter().map(|point| point.to_x_coordinate())); 184 message.push(*tag); 185 } 186 // An external record input is hashed (using `tvk`) to a field element. 187 InputID::ExternalRecord(input_hash) => { 188 // Ensure the input is a record. 189 ensure!(matches!(input, Value::Record(..)), "Expected a record input"); 190 191 // Construct the (console) input index as a field element. 192 let index = Field::from_u16(u16::try_from(index).or_halt_with::<N>("Input index exceeds u16")); 193 // Construct the preimage as `(function ID || input || tvk || index)`. 194 let mut preimage = Vec::new(); 195 preimage.push(function_id); 196 preimage.extend(input.to_fields()?); 197 preimage.push(self.tvk); 198 preimage.push(index); 199 // Hash the input to a field element. 200 let candidate_hash = N::hash_psd8(&preimage)?; 201 // Ensure the input hash matches. 202 ensure!(*input_hash == candidate_hash, "Expected a locator input with the same hash"); 203 204 // Add the input hash to the message. 205 message.push(candidate_hash); 206 } 207 } 208 Ok(()) 209 }, 210 ) { 211 eprintln!("Request verification failed on input checks: {error}"); 212 return false; 213 } 214 215 // Verify the signature. 216 self.signature.verify(&self.signer, &message) 217 } 218 } 219 220 #[cfg(test)] 221 mod tests { 222 use super::*; 223 use alphavm_console_account::PrivateKey; 224 use alphavm_console_network::MainnetV0; 225 226 type CurrentNetwork = MainnetV0; 227 228 pub(crate) const ITERATIONS: usize = 1000; 229 230 #[test] 231 fn test_sign_and_verify() { 232 let rng = &mut TestRng::default(); 233 234 for i in 0..ITERATIONS { 235 // Sample a random private key and address. 236 let private_key = PrivateKey::<CurrentNetwork>::new(rng).unwrap(); 237 let address = Address::try_from(&private_key).unwrap(); 238 239 // Construct a program ID and function name. 240 let program_id = ProgramID::from_str("token.alpha").unwrap(); 241 let function_name = Identifier::from_str("transfer").unwrap(); 242 243 // Prepare a record belonging to the address. 244 let record_string = format!( 245 "{{ owner: {address}.private, token_amount: 100u64.private, _nonce: 2293253577170800572742339369209137467208538700597121244293392265726446806023group.public }}" 246 ); 247 248 // Construct four inputs. 249 let input_constant = Value::from_str("{ token_amount: 9876543210u128 }").unwrap(); 250 let input_public = Value::from_str("{ token_amount: 9876543210u128 }").unwrap(); 251 let input_private = Value::from_str("{ token_amount: 9876543210u128 }").unwrap(); 252 let input_record = Value::from_str(&record_string).unwrap(); 253 let input_external_record = Value::from_str(&record_string).unwrap(); 254 let inputs = [input_constant, input_public, input_private, input_record, input_external_record]; 255 256 // Construct the input types. 257 let input_types = vec![ 258 ValueType::from_str("amount.constant").unwrap(), 259 ValueType::from_str("amount.public").unwrap(), 260 ValueType::from_str("amount.private").unwrap(), 261 ValueType::from_str("token.record").unwrap(), 262 ValueType::from_str("token.alpha/token.record").unwrap(), 263 ]; 264 265 // Sample 'root_tvk'. 266 let root_tvk = None; 267 // Sample 'is_root'. 268 let is_root = Uniform::rand(rng); 269 // Sample 'program_checksum'. 270 let program_checksum = match i % 2 == 0 { 271 true => Some(Field::rand(rng)), 272 false => None, 273 }; 274 275 // Compute the signed request. 276 let request = Request::sign( 277 &private_key, 278 program_id, 279 function_name, 280 inputs.into_iter(), 281 &input_types, 282 root_tvk, 283 is_root, 284 program_checksum, 285 rng, 286 ) 287 .unwrap(); 288 assert!(request.verify(&input_types, is_root, program_checksum)); 289 } 290 } 291 }