snarkvm_console_program/request/verify.rs
1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkVM 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
16use super::*;
17
18impl<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)]
221mod tests {
222 use super::*;
223 use snarkvm_console_account::PrivateKey;
224 use snarkvm_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.aleo").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.aleo/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}