Skip to main content

snarkvm_synthesizer_process/
verify_fee.rs

1// Copyright (c) 2019-2026 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> Process<N> {
19    /// Verifies the given fee is valid.
20    /// Note: This does *not* check that the global state root exists in the ledger.
21    #[inline]
22    pub fn verify_fee(
23        &self,
24        consensus_version: ConsensusVersion,
25        varuna_version: VarunaVersion,
26        inclusion_version: InclusionVersion,
27        fee: &Fee<N>,
28        deployment_or_execution_id: Field<N>,
29    ) -> Result<()> {
30        let timer = timer!("Process::verify_fee");
31
32        // Retrieve the stack.
33        let stack = self.get_stack(fee.program_id())?;
34        // Retrieve the function from the stack.
35        let function = stack.get_function(fee.function_name())?;
36
37        dev_println!("Verifying fee from {}/{}...", fee.program_id(), fee.function_name());
38
39        #[cfg(debug_assertions)]
40        {
41            // Ensure that there are no dynamic calls in this function.
42            if stack.contains_dynamic_call(function.name())? {
43                bail!("The function '{}/{}' should not have dynamic calls", stack.program_id(), function.name())
44            }
45            // Ensure the minimum number of function calls in this function is 1.
46            if stack.get_minimum_number_of_calls(function.name())? != 1 {
47                bail!("The number of function calls in '{}/{}' should be 1", stack.program_id(), function.name())
48            }
49            // Debug-mode only, as the `Transition` constructor recomputes the transition ID at initialization.
50            let expected_id = N::hash_bhp512(&(fee.to_root()?, *fee.tcm()).to_bits_le())?;
51            debug_assert_eq!(**fee.id(), expected_id, "Transition ID of the fee is incorrect");
52        }
53
54        // Determine if the fee is private.
55        let is_fee_private = fee.is_fee_private();
56        // Determine if the fee is public.
57        let is_fee_public = fee.is_fee_public();
58        // Ensure the fee has the correct program ID and function.
59        ensure!(is_fee_private || is_fee_public, "Incorrect program ID or function name for fee transition");
60        // Ensure the number of inputs is within the allowed range.
61        ensure!(fee.inputs().len() <= N::MAX_INPUTS, "Fee exceeded maximum number of inputs");
62        // Ensure the number of outputs is within the allowed range.
63        ensure!(fee.outputs().len() <= N::MAX_INPUTS, "Fee exceeded maximum number of outputs");
64
65        // Ensure the input and output types are equivalent to the ones defined in the function.
66        // We only need to check that the variant type matches because we already check the hashes in
67        // the `Input::verify` and `Output::verify` functions.
68        ensure!(
69            function.input_types().len() == fee.inputs().len(),
70            "The number of fee inputs is incorrect: expected {}, found {}",
71            function.input_types().len(),
72            fee.inputs().len()
73        );
74        for (function_input, fee_input) in function.input_types().iter().zip_eq(fee.inputs().iter()) {
75            ensure!(fee_input.is_type(function_input), "The fee input variants do not match");
76        }
77        ensure!(
78            function.output_types().len() == fee.outputs().len(),
79            "The number of fee outputs is incorrect: expected {}, found {}",
80            function.output_types().len(),
81            fee.outputs().len()
82        );
83        for (function_output, fee_output) in function.output_types().iter().zip_eq(fee.outputs().iter()) {
84            ensure!(fee_output.is_type(function_output), "The fee output variants do not match");
85        }
86
87        // Retrieve the candidate deployment or execution ID.
88        let Ok(candidate_id) = fee.deployment_or_execution_id() else {
89            bail!("Failed to get the deployment or execution ID in the fee transition")
90        };
91        // Ensure the candidate ID is the deployment or execution ID.
92        if candidate_id != deployment_or_execution_id {
93            bail!("Incorrect deployment or execution ID in the fee transition")
94        }
95        lap!(timer, "Verify the deployment or execution ID");
96
97        // Verify the fee transition is well-formed.
98        match is_fee_private {
99            true => self.verify_fee_private(consensus_version, varuna_version, inclusion_version, &fee)?,
100            false => self.verify_fee_public(varuna_version, inclusion_version, &fee)?,
101        }
102        finish!(timer, "Verify the fee transition");
103        Ok(())
104    }
105}
106
107impl<N: Network> Process<N> {
108    /// Verifies the transition for `credits.aleo/fee_private` is well-formed.
109    fn verify_fee_private(
110        &self,
111        consensus_version: ConsensusVersion,
112        varuna_version: VarunaVersion,
113        inclusion_version: InclusionVersion,
114        fee: &&Fee<N>,
115    ) -> Result<()> {
116        let timer = timer!("Process::verify_fee_private");
117
118        // Retrieve the network ID.
119        let network_id = U16::new(N::ID);
120        // Compute the function ID.
121        let function_id = compute_function_id(&network_id, fee.program_id(), fee.function_name())?;
122
123        // Ensure the fee contains 1 input record.
124        ensure!(
125            fee.inputs().iter().filter(|input| matches!(input, Input::Record(..))).count() == 1,
126            "The fee transition must contain *1* input record"
127        );
128        // Ensure the number of inputs is correct.
129        let num_inputs = fee.inputs().len();
130        ensure!(num_inputs == 4, "The number of inputs in the fee transition should be 4, found {num_inputs}",);
131        // Ensure each input is valid.
132        if fee.inputs().iter().enumerate().any(|(index, input)| !input.verify(function_id, fee.tcm(), index)) {
133            bail!("Failed to verify a fee input")
134        }
135        lap!(timer, "Verify the inputs");
136
137        // Ensure the number of outputs is correct.
138        ensure!(
139            fee.outputs().len() == 1,
140            "The number of outputs in the fee transition should be 1, found {}",
141            fee.outputs().len()
142        );
143        // Ensure each output is valid.
144        for (index, output) in fee.outputs().iter().enumerate() {
145            // If the consensus version are before `ConsensusVersion::V8`, ensure the output record is on Version 0.
146            // if the consensus version is on or after `ConsensusVersion::V8`, ensure the output record is on Version 1.
147            if let Some((_, record)) = output.record() {
148                if (ConsensusVersion::V1..=ConsensusVersion::V7).contains(&consensus_version) {
149                    #[cfg(not(any(test, feature = "test")))]
150                    ensure!(record.version().is_zero(), "Output record must be Version 0 before Consensus V8");
151                    #[cfg(any(test, feature = "test"))]
152                    ensure!(
153                        record.version().is_one(),
154                        "Output record must be Version 1 before Consensus V8  in tests."
155                    );
156                } else {
157                    ensure!(record.version().is_one(), "Output record must be Version 1 on or after Consensus V8");
158                }
159            }
160            // Ensure the output is valid.
161            if !output.verify(function_id, fee.tcm(), num_inputs + index) {
162                bail!("Failed to verify a fee output")
163            }
164        }
165        lap!(timer, "Verify the outputs");
166
167        // Compute the x- and y-coordinate of `tpk`.
168        let (tpk_x, tpk_y) = fee.tpk().to_xy_coordinates();
169
170        // Retrieve the address belonging to the program ID.
171        let stack = self.get_stack(fee.program_id())?;
172        let program_address = stack.program_address();
173
174        // Compute the x- and y-coordinate of `parent`.
175        let (parent_x, parent_y) = program_address.to_xy_coordinates();
176
177        // Construct the public inputs to verify the proof.
178        let mut inputs = vec![N::Field::one(), *tpk_x, *tpk_y, **fee.tcm(), **fee.scm()];
179        // Extend the inputs with the input IDs.
180        inputs.extend(fee.inputs().iter().flat_map(|input| input.verifier_inputs()));
181        // Extend the verifier inputs with the public inputs for 'self.caller'.
182        inputs.extend([*Field::<N>::one(), *parent_x, *parent_y]);
183        // Extend the inputs with the output IDs.
184        inputs.extend(fee.outputs().iter().flat_map(|output| output.verifier_inputs()));
185        lap!(timer, "Construct the verifier inputs");
186
187        dev_println!("Fee public inputs ({} elements): {:#?}", inputs.len(), inputs);
188
189        // Retrieve the verifying key.
190        let verifying_key = stack.get_verifying_key(fee.function_name())?;
191
192        // Ensure the fee proof is valid.
193        Trace::verify_fee_proof(varuna_version, inclusion_version, (verifying_key, vec![inputs]), fee)?;
194        finish!(timer, "Verify the fee proof");
195        Ok(())
196    }
197
198    /// Verifies the transition for `credits.aleo/fee_public` is well-formed.
199    /// Attention: This method does *not* verify the account balance is sufficient.
200    fn verify_fee_public(
201        &self,
202        varuna_version: VarunaVersion,
203        inclusion_version: InclusionVersion,
204        fee: &&Fee<N>,
205    ) -> Result<()> {
206        let timer = timer!("Process::verify_fee_public");
207
208        // Retrieve the network ID.
209        let network_id = U16::new(N::ID);
210        // Compute the function ID.
211        let function_id = compute_function_id(&network_id, fee.program_id(), fee.function_name())?;
212
213        // Ensure the fee contains all public inputs.
214        ensure!(
215            fee.inputs().iter().all(|input| matches!(input, Input::Public(..))),
216            "The fee transition must contain *only* public inputs"
217        );
218        // Ensure the number of inputs is correct.
219        let num_inputs = fee.inputs().len();
220        ensure!(num_inputs == 3, "The number of inputs in the fee transition should be 3, found {num_inputs}",);
221        // Ensure each input is valid.
222        if fee.inputs().iter().enumerate().any(|(index, input)| !input.verify(function_id, fee.tcm(), index)) {
223            bail!("Failed to verify a fee input")
224        }
225        lap!(timer, "Verify the inputs");
226
227        // Ensure there is one output.
228        ensure!(
229            fee.outputs().len() == 1,
230            "The number of outputs in the fee transition should be 1, found {}",
231            fee.outputs().len()
232        );
233        // Ensure each output is valid.
234        if fee
235            .outputs()
236            .iter()
237            .enumerate()
238            .any(|(index, output)| !output.verify(function_id, fee.tcm(), num_inputs + index))
239        {
240            bail!("Failed to verify a fee output")
241        }
242        lap!(timer, "Verify the outputs");
243
244        // Compute the x- and y-coordinate of `tpk`.
245        let (tpk_x, tpk_y) = fee.tpk().to_xy_coordinates();
246
247        // Retrieve the address belonging to the program ID.
248        let stack = self.get_stack(fee.program_id())?;
249        let program_address = stack.program_address();
250
251        // Compute the x- and y-coordinate of `parent`.
252        let (parent_x, parent_y) = program_address.to_xy_coordinates();
253
254        // Construct the public inputs to verify the proof.
255        let mut inputs = vec![N::Field::one(), *tpk_x, *tpk_y, **fee.tcm(), **fee.scm()];
256        // Extend the inputs with the input IDs.
257        inputs.extend(fee.inputs().iter().flat_map(|input| input.verifier_inputs()));
258        // Extend the verifier inputs with the public inputs for 'self.caller'
259        inputs.extend([*Field::<N>::one(), *parent_x, *parent_y]);
260        // Extend the inputs with the output IDs.
261        inputs.extend(fee.outputs().iter().flat_map(|output| output.verifier_inputs()));
262        lap!(timer, "Construct the verifier inputs");
263
264        dev_println!("Fee public inputs ({} elements): {:#?}", inputs.len(), inputs);
265
266        // Retrieve the verifying key.
267        let verifying_key = stack.get_verifying_key(fee.function_name())?;
268
269        // Ensure the fee proof is valid.
270        Trace::verify_fee_proof(varuna_version, inclusion_version, (verifying_key, vec![inputs]), fee)?;
271        finish!(timer, "Verify the fee proof");
272        Ok(())
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use console::prelude::TestRng;
280    use snarkvm_ledger_block::Transaction;
281
282    #[test]
283    fn test_verify_fee() {
284        let rng = &mut TestRng::default();
285
286        // Fetch transactions.
287        let transactions = [
288            snarkvm_ledger_test_helpers::sample_deployment_transaction(1, Uniform::rand(rng), false, true, rng),
289            snarkvm_ledger_test_helpers::sample_deployment_transaction(1, Uniform::rand(rng), false, false, rng),
290            snarkvm_ledger_test_helpers::sample_deployment_transaction(2, Uniform::rand(rng), false, true, rng),
291            snarkvm_ledger_test_helpers::sample_deployment_transaction(2, Uniform::rand(rng), false, false, rng),
292            snarkvm_ledger_test_helpers::sample_deployment_transaction(2, Uniform::rand(rng), true, true, rng),
293            snarkvm_ledger_test_helpers::sample_deployment_transaction(2, Uniform::rand(rng), true, false, rng),
294            snarkvm_ledger_test_helpers::sample_execution_transaction_with_fee(true, rng, 0),
295            snarkvm_ledger_test_helpers::sample_execution_transaction_with_fee(false, rng, 0),
296            snarkvm_ledger_test_helpers::sample_fee_private_transaction(rng),
297            snarkvm_ledger_test_helpers::sample_fee_public_transaction(rng),
298        ];
299
300        // Construct a new process.
301        let process = Process::load().unwrap();
302
303        for transaction in transactions {
304            match transaction {
305                Transaction::Deploy(_, _, _, deployment, fee) => {
306                    // Compute the deployment ID.
307                    let deployment_id = deployment.to_deployment_id().unwrap();
308                    // Verify the fee.
309                    process
310                        .verify_fee(ConsensusVersion::V8, VarunaVersion::V1, InclusionVersion::V0, &fee, deployment_id)
311                        .unwrap();
312                }
313                Transaction::Execute(_, _, execution, fee) => {
314                    // Compute the execution ID.
315                    let execution_id = execution.to_execution_id().unwrap();
316                    // Verify the fee.
317                    process
318                        .verify_fee(
319                            ConsensusVersion::V8,
320                            VarunaVersion::V1,
321                            InclusionVersion::V0,
322                            &fee.unwrap(),
323                            execution_id,
324                        )
325                        .unwrap();
326                }
327                Transaction::Fee(_, fee) => match fee.is_fee_private() {
328                    true => process
329                        .verify_fee_private(ConsensusVersion::V8, VarunaVersion::V1, InclusionVersion::V0, &&fee)
330                        .unwrap(),
331                    false => process.verify_fee_public(VarunaVersion::V1, InclusionVersion::V0, &&fee).unwrap(),
332                },
333            }
334        }
335    }
336}