/ console / program / src / request / verify.rs
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  }