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}