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