Skip to main content

snarkvm_synthesizer_process/
cost.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 std::collections::HashMap;
17
18use crate::{Authorization, FinalizeTypes, Process, Stack, StackRef, StackTrait};
19
20use console::{
21    prelude::*,
22    program::{FinalizeType, Identifier, LiteralType, PlaintextType},
23};
24use snarkvm_algorithms::snark::varuna::VarunaVersion;
25use snarkvm_ledger_block::{Deployment, Execution, Transaction};
26use snarkvm_synthesizer_program::{CallDynamic, CastType, Command, GetRecordDynamic, Instruction, Operand};
27use snarkvm_synthesizer_snark::proof_size;
28
29pub type MinimumCost = u64;
30pub type StorageCost = u64;
31pub type SynthesisCost = u64;
32pub type ConstructorCost = u64;
33pub type NamespaceCost = u64;
34pub type FinalizeCost = u64;
35pub type DeployCostDetails = (StorageCost, SynthesisCost, ConstructorCost, NamespaceCost);
36pub type ExecuteCostDetails = (StorageCost, FinalizeCost);
37
38/// Returns the deployment cost in microcredits for a given deployment.
39pub fn deployment_cost<N: Network>(
40    process: &Process<N>,
41    deployment: &Deployment<N>,
42    consensus_version: ConsensusVersion,
43) -> Result<(MinimumCost, DeployCostDetails)> {
44    if consensus_version >= ConsensusVersion::V10 {
45        deployment_cost_v2(process, deployment)
46    } else {
47        deployment_cost_v1(process, deployment)
48    }
49}
50
51/// Returns the execution cost in microcredits for a given execution.
52pub fn execution_cost<N: Network>(
53    process: &Process<N>,
54    execution: &Execution<N>,
55    consensus_version: ConsensusVersion,
56) -> Result<(MinimumCost, ExecuteCostDetails)> {
57    let execution_size = execution.size_in_bytes()?;
58
59    execution_cost_given_size(process, execution, execution_size, consensus_version)
60}
61
62// Returns the execution cost in microcredits for a given execution whose size is provided as an argument.
63fn execution_cost_given_size<N: Network>(
64    process: &Process<N>,
65    execution: &Execution<N>,
66    execution_size: u64,
67    consensus_version: ConsensusVersion,
68) -> Result<(MinimumCost, ExecuteCostDetails)> {
69    if consensus_version >= ConsensusVersion::V10 {
70        execution_cost_v3(process, execution, execution_size)
71    } else if consensus_version >= ConsensusVersion::V2 {
72        execution_cost_v2(process, execution, execution_size)
73    } else {
74        execution_cost_v1(process, execution, execution_size)
75    }
76}
77
78/// Returns the execution cost in microcredits for a given `Authorization.
79pub fn execution_cost_for_authorization<N: Network>(
80    process: &Process<N>,
81    authorization: &Authorization<N>,
82    consensus_version: ConsensusVersion,
83) -> Result<(MinimumCost, ExecuteCostDetails)> {
84    ensure!(
85        consensus_version >= ConsensusVersion::V4,
86        "Execution-cost computation for authorization relies on proof-size estimation, which is only implemented for Varuna version >= V2 (consensus version >= V4)"
87    );
88
89    // Reconstruct an Execution from the Authorization. Note that the StateRoot
90    // does not affect the fee (it has constant size).
91    let reconstructed_execution =
92        Execution::from(authorization.transitions().values().cloned(), N::StateRoot::default(), None)?;
93
94    // Compute the size of the proof that will result from proving the
95    // Authorization. The first step is to compute the Varuna batch sizes. The
96    // Varuna circuits that must be proved as part of an Execution are:
97    // - the circuit instances of each Transition
98    // - one inclusion circuit instance per input record to all of those Transitions
99    // - one translation circuit instance per record-translation task derived from those Transitions
100    // For each of the types above, if several instances correspond to the same circuit, they are
101    // grouped into a single Varuna batch.
102
103    let mut circuit_frequencies = HashMap::new();
104
105    // In order to compute the frequencies of function circuits, we mimic the
106    // operation of Process::verify_execution:
107    for transition in authorization.transitions().values() {
108        let entry =
109            circuit_frequencies.entry((*transition.program_id(), *transition.function_name())).or_insert(0usize);
110        *entry += 1;
111    }
112
113    let mut batch_sizes: Vec<usize> = circuit_frequencies.values().cloned().collect();
114
115    // Add the single batch of inclusion circuits for input records, if
116    // any:
117    let n_input_records = Authorization::number_of_input_records(authorization.transitions().values());
118    if n_input_records > 0 {
119        batch_sizes.push(n_input_records);
120    }
121
122    // Add the batches corresponding to translation tasks
123    let translations_for_transaction =
124        Authorization::translation_batch_sizes(process, authorization.transitions().values())?;
125    batch_sizes.extend(translations_for_transaction);
126
127    // Varuna is always run in hiding (i. e. ZK) mode when proving Executions.
128    let hiding_mode = true;
129
130    // If future versions of Varuna are introduced, the correct one should be
131    // deduced here from the consensus version. Currently only the latest Varuna
132    // version V2 is supported.
133    let varuna_version = VarunaVersion::V2;
134
135    let expected_proof_size = u64::try_from(proof_size::<N>(&batch_sizes, varuna_version, hiding_mode)?)?;
136    let unproved_execution_size = reconstructed_execution.size_in_bytes()?;
137    let execution_size = unproved_execution_size.checked_add(expected_proof_size).ok_or(anyhow!(
138        "The execution size computation overflowed for an authorization when the proof was taken into account"
139    ))?;
140
141    execution_cost_given_size(process, &reconstructed_execution, execution_size, consensus_version)
142}
143
144/// Returns the compute cost for a deployment in microcredits.
145/// This is used to limit the amount of single-threaded compute in the block generation hot
146/// path. This does NOT represent the full costs which a user has to pay.
147pub fn deploy_compute_cost_in_microcredits(
148    cost_details: DeployCostDetails,
149    consensus_version: ConsensusVersion,
150) -> Result<u64> {
151    let (storage_cost, synthesis_cost, constructor_cost, _) = cost_details;
152    let cost_to_check = if consensus_version >= ConsensusVersion::V10 {
153        // From V10, only include the constructor compute cost for
154        // deployments.
155        //
156        // The limits of individual function's finalize compute costs are
157        // checked in calls to `deployment_cost`.
158        constructor_cost
159    } else {
160        // Include the storage, synthesis, and constructor cost for deployments.
161        storage_cost
162            .checked_add(synthesis_cost)
163            .and_then(|synthesis_cost| synthesis_cost.checked_add(constructor_cost))
164            .ok_or(anyhow!("The storage, synthesis, and constructor cost computation overflowed for a deployment"))?
165    };
166    Ok(cost_to_check)
167}
168
169/// Returns the compute cost for an execution in microcredits.
170/// This is used to limit the amount of single-threaded compute in the block generation hot
171/// path. This does NOT represent the full costs which a user has to pay.
172pub fn execute_compute_cost_in_microcredits(
173    cost_details: ExecuteCostDetails,
174    consensus_version: ConsensusVersion,
175) -> Result<u64> {
176    let (storage_cost, finalize_cost) = cost_details;
177    let cost_to_check = if consensus_version >= ConsensusVersion::V10 {
178        // From V10, only include the finalize compute cost for executions.
179        finalize_cost
180    } else {
181        // Include the finalize cost and storage cost for executions.
182        storage_cost
183            .checked_add(finalize_cost)
184            .ok_or(anyhow!("The storage and finalize cost computation overflowed for an execution"))?
185    };
186    Ok(cost_to_check)
187}
188
189/// Returns the *minimum* cost in microcredits to publish the given deployment using the ARC_0005_COMPUTE_DISCOUNT.
190pub fn deployment_cost_v2<N: Network>(
191    process: &Process<N>,
192    deployment: &Deployment<N>,
193) -> Result<(MinimumCost, DeployCostDetails)> {
194    // Determine the number of bytes in the deployment.
195    let size_in_bytes = deployment.size_in_bytes()?;
196    // Retrieve the program ID.
197    let program_id = deployment.program_id();
198    // Determine the number of characters in the program ID.
199    let num_characters = u32::try_from(program_id.name().to_string().len())?;
200    // Compute the number of combined variables in the program.
201    let num_combined_variables = deployment.num_combined_variables()?;
202    // Compute the number of combined constraints in the program.
203    let num_combined_constraints = deployment.num_combined_constraints()?;
204
205    // Compute the storage cost in microcredits.
206    let storage_cost = size_in_bytes
207        .checked_mul(N::DEPLOYMENT_FEE_MULTIPLIER)
208        .ok_or(anyhow!("The storage cost computation overflowed for a deployment"))?;
209
210    // Compute the synthesis cost in microcredits.
211    let synthesis_cost = num_combined_variables.saturating_add(num_combined_constraints) * N::SYNTHESIS_FEE_MULTIPLIER
212        / N::ARC_0005_COMPUTE_DISCOUNT;
213
214    // Compute a Stack for the deployment.
215    let stack = Stack::new(process, deployment.program())?;
216
217    // Compute the constructor cost in microcredits.
218    let constructor_cost = constructor_cost_in_microcredits_v2(&stack)?;
219
220    // Check that the functions are valid.
221    for function in deployment.program().functions().values() {
222        // Get the finalize cost.
223        let finalize_cost = minimum_cost_in_microcredits_v3(&stack, function.name())?;
224        // Check that the finalize cost does not exceed the maximum.
225        ensure!(
226            finalize_cost <= N::TRANSACTION_SPEND_LIMIT[1].1,
227            "Finalize block '{}' has a cost '{finalize_cost}' which exceeds the transaction spend limit '{}'",
228            function.name(),
229            N::TRANSACTION_SPEND_LIMIT[1].1
230        );
231    }
232
233    // Compute the namespace cost in microcredits: 10^(10 - num_characters) * 1e6
234    let namespace_cost = 10u64
235        .checked_pow(10u32.saturating_sub(num_characters))
236        .ok_or(anyhow!("The namespace cost computation overflowed for a deployment"))?
237        .saturating_mul(1_000_000); // 1 microcredit = 1e-6 credits.
238
239    // Compute the minimum cost in microcredits.
240    let minimum_cost = storage_cost
241        .checked_add(synthesis_cost)
242        .and_then(|x| x.checked_add(constructor_cost))
243        .and_then(|x| x.checked_add(namespace_cost))
244        .ok_or(anyhow!("The total cost computation overflowed for a deployment"))?;
245
246    Ok((minimum_cost, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)))
247}
248
249/// Returns the *minimum* cost in microcredits to publish the given deployment.
250pub fn deployment_cost_v1<N: Network>(
251    process: &Process<N>,
252    deployment: &Deployment<N>,
253) -> Result<(MinimumCost, DeployCostDetails)> {
254    // Determine the number of bytes in the deployment.
255    let size_in_bytes = deployment.size_in_bytes()?;
256    // Retrieve the program ID.
257    let program_id = deployment.program_id();
258    // Determine the number of characters in the program ID.
259    let num_characters = u32::try_from(program_id.name().to_string().len())?;
260    // Compute the number of combined variables in the program.
261    let num_combined_variables = deployment.num_combined_variables()?;
262    // Compute the number of combined constraints in the program.
263    let num_combined_constraints = deployment.num_combined_constraints()?;
264
265    // Compute the storage cost in microcredits.
266    let storage_cost = size_in_bytes
267        .checked_mul(N::DEPLOYMENT_FEE_MULTIPLIER)
268        .ok_or(anyhow!("The storage cost computation overflowed for a deployment"))?;
269
270    // Compute the synthesis cost in microcredits.
271    let synthesis_cost = num_combined_variables.saturating_add(num_combined_constraints) * N::SYNTHESIS_FEE_MULTIPLIER;
272
273    // Compute a Stack for the deployment.
274    let stack = Stack::new(process, deployment.program())?;
275
276    // Compute the constructor cost in microcredits.
277    let constructor_cost = constructor_cost_in_microcredits_v1(&stack)?;
278
279    // Check that the functions are valid.
280    for function in deployment.program().functions().values() {
281        // Get the finalize cost.
282        let finalize_cost = minimum_cost_in_microcredits_v2(&stack, function.name())?;
283        // Check that the finalize cost does not exceed the maximum.
284        ensure!(
285            finalize_cost <= N::TRANSACTION_SPEND_LIMIT[0].1,
286            "Finalize block '{}' has a cost '{finalize_cost}' which exceeds the transaction spend limit '{}'",
287            function.name(),
288            N::TRANSACTION_SPEND_LIMIT[0].1
289        );
290    }
291
292    // Compute the namespace cost in microcredits: 10^(10 - num_characters) * 1e6
293    let namespace_cost = 10u64
294        .checked_pow(10u32.saturating_sub(num_characters))
295        .ok_or(anyhow!("The namespace cost computation overflowed for a deployment"))?
296        .saturating_mul(1_000_000); // 1 microcredit = 1e-6 credits.
297
298    // Compute the minimum cost in microcredits.
299    let minimum_cost = storage_cost
300        .checked_add(synthesis_cost)
301        .and_then(|x| x.checked_add(constructor_cost))
302        .and_then(|x| x.checked_add(namespace_cost))
303        .ok_or(anyhow!("The total cost computation overflowed for a deployment"))?;
304
305    Ok((minimum_cost, (storage_cost, synthesis_cost, constructor_cost, namespace_cost)))
306}
307
308/// Returns the cost in microcredits to publish the given execution using the ARC_0005_COMPUTE_DISCOUNT.
309/// For executions with dynamic futures, this computes the exact cost by iterating over concrete transitions.
310fn execution_cost_v3<N: Network>(
311    process: &Process<N>,
312    execution: &Execution<N>,
313    execution_size: u64,
314) -> Result<(MinimumCost, ExecuteCostDetails)> {
315    // Compute the storage cost in microcredits.
316    let storage_cost = execution_storage_cost::<N>(execution_size);
317
318    // Compute the finalize cost by iterating over all concrete transitions.
319    // This handles dynamic futures correctly because we know the actual functions called.
320    let finalize_cost = execution_finalize_cost(process, execution, ConsensusFeeVersion::V3)?;
321
322    // Compute the total cost in microcredits.
323    let total_cost = storage_cost
324        .checked_add(finalize_cost)
325        .ok_or(anyhow!("The total cost computation overflowed for an execution"))?;
326
327    Ok((total_cost, (storage_cost, finalize_cost)))
328}
329
330/// Returns the cost in microcredits to publish the given execution.
331/// For executions with dynamic futures, this computes the exact cost by iterating over concrete transitions.
332fn execution_cost_v2<N: Network>(
333    process: &Process<N>,
334    execution: &Execution<N>,
335    execution_size: u64,
336) -> Result<(MinimumCost, ExecuteCostDetails)> {
337    // Compute the storage cost in microcredits.
338    let storage_cost = execution_storage_cost::<N>(execution_size);
339
340    // Compute the finalize cost by iterating over all concrete transitions.
341    // This handles dynamic futures correctly because we know the actual functions called.
342    let finalize_cost = execution_finalize_cost(process, execution, ConsensusFeeVersion::V2)?;
343
344    // Compute the total cost in microcredits.
345    let total_cost = storage_cost
346        .checked_add(finalize_cost)
347        .ok_or(anyhow!("The total cost computation overflowed for an execution"))?;
348
349    Ok((total_cost, (storage_cost, finalize_cost)))
350}
351
352/// Returns the cost in microcredits to publish the given execution.
353/// For executions with dynamic futures, this computes the exact cost by iterating over concrete transitions.
354fn execution_cost_v1<N: Network>(
355    process: &Process<N>,
356    execution: &Execution<N>,
357    execution_size: u64,
358) -> Result<(MinimumCost, ExecuteCostDetails)> {
359    // Compute the storage cost in microcredits.
360    let storage_cost = execution_storage_cost::<N>(execution_size);
361
362    // Compute the finalize cost by iterating over all concrete transitions.
363    // This handles dynamic futures correctly because we know the actual functions called.
364    let finalize_cost = execution_finalize_cost(process, execution, ConsensusFeeVersion::V1)?;
365
366    // Compute the total cost in microcredits.
367    let total_cost = storage_cost
368        .checked_add(finalize_cost)
369        .ok_or(anyhow!("The total cost computation overflowed for an execution"))?;
370
371    Ok((total_cost, (storage_cost, finalize_cost)))
372}
373
374/// Returns the storage cost in microcredits for a program execution.
375fn execution_storage_cost<N: Network>(size_in_bytes: u64) -> u64 {
376    if size_in_bytes > N::EXECUTION_STORAGE_PENALTY_THRESHOLD {
377        size_in_bytes.saturating_mul(size_in_bytes).saturating_div(N::EXECUTION_STORAGE_FEE_SCALING_FACTOR)
378    } else {
379        size_in_bytes
380    }
381}
382
383// Finalize costs for compute heavy operations, derived as:
384// `BASE_COST + (PER_BYTE_COST * SIZE_IN_BYTES)`.
385
386const CAST_BASE_COST: u64 = 500;
387const CAST_PER_BYTE_COST: u64 = 30;
388
389const HASH_BASE_COST: u64 = 10_000;
390const HASH_PER_BYTE_COST: u64 = 30;
391
392const HASH_BHP_BASE_COST: u64 = 50_000;
393const HASH_BHP_PER_BYTE_COST: u64 = 300;
394
395const HASH_PSD_BASE_COST: u64 = 40_000;
396const HASH_PSD_PER_BYTE_COST: u64 = 75;
397
398const ECDSA_VERIFY_BASE_COST: u64 = 60_000;
399const ECDSA_VERIFY_ETH_BASE_COST: u64 = 75_000;
400
401const SNARK_VERIFY_BASE_COST: u64 = 100_000;
402const SNARK_VERIFY_PER_BYTE_COST: u64 = 50;
403
404#[derive(Copy, Clone)]
405pub enum ConsensusFeeVersion {
406    V1,
407    V2,
408    V3,
409}
410
411const MAPPING_BASE_COST_V1: u64 = 10_000;
412const MAPPING_BASE_COST_V2: u64 = 1_500;
413const MAPPING_PER_BYTE_COST: u64 = 10;
414
415const SET_BASE_COST: u64 = 10_000;
416const SET_PER_BYTE_COST: u64 = 100;
417
418/// A helper function to determine the plaintext type in bytes.
419fn plaintext_size_in_bytes<N: Network>(stack: &Stack<N>, plaintext_type: &PlaintextType<N>) -> Result<u64> {
420    match plaintext_type {
421        PlaintextType::Literal(literal_type) => Ok(literal_type.size_in_bytes::<N>() as u64),
422        PlaintextType::Struct(struct_name) => {
423            // Retrieve the struct from the stack.
424            let struct_ = stack.program().get_struct(struct_name)?;
425            // Retrieve the size of the struct name.
426            let size_of_name = struct_.name().to_bytes_le()?.len() as u64;
427            // Retrieve the size of all the members of the struct.
428            let size_of_members = struct_.members().iter().try_fold(0u64, |acc, (_, member_type)| {
429                acc.checked_add(plaintext_size_in_bytes(stack, member_type)?).ok_or(anyhow!(
430                    "Overflowed while computing the size of the struct '{}/{struct_name}' - {member_type}",
431                    stack.program_id()
432                ))
433            })?;
434            // Return the size of the struct.
435            Ok(size_of_name.saturating_add(size_of_members))
436        }
437        PlaintextType::ExternalStruct(locator) => {
438            let external_stack = stack.get_external_stack(locator.program_id())?;
439            plaintext_size_in_bytes(&*external_stack, &PlaintextType::Struct(*locator.resource()))
440        }
441        PlaintextType::Array(array_type) => {
442            // Retrieve the number of elements in the array.
443            let num_elements = **array_type.length() as u64;
444            // Compute the size of an array element.
445            let size_of_element = plaintext_size_in_bytes(stack, array_type.next_element_type())?;
446            // Return the size of the array.
447            Ok(num_elements.saturating_mul(size_of_element))
448        }
449    }
450}
451
452/// A helper function to compute the following: base_cost + (byte_multiplier * size_of_operands).
453fn cost_in_size<'a, N: Network>(
454    stack: &Stack<N>,
455    finalize_types: &FinalizeTypes<N>,
456    operands: impl IntoIterator<Item = &'a Operand<N>>,
457    byte_multiplier: u64,
458    base_cost: u64,
459) -> Result<u64> {
460    // Compute the size of the operands.
461    let size_of_operands = operands.into_iter().try_fold(0u64, |acc, operand| {
462        // Determine the size of the operand.
463        let operand_size = match finalize_types.get_type_from_operand(stack, operand)? {
464            FinalizeType::Plaintext(plaintext_type) => plaintext_size_in_bytes(stack, &plaintext_type)?,
465            FinalizeType::Future(future) => {
466                bail!("Future '{future}' is not a valid operand");
467            }
468            FinalizeType::DynamicFuture => {
469                bail!("Dynamic future is not a valid operand");
470            }
471        };
472        // Safely add the size to the accumulator.
473        acc.checked_add(operand_size).ok_or(anyhow!(
474            "Overflowed while computing the size of the operand '{operand}' in '{}'",
475            stack.program_id(),
476        ))
477    })?;
478    // Return the cost.
479    Ok(base_cost.saturating_add(byte_multiplier.saturating_mul(size_of_operands)))
480}
481
482/// Returns the the cost of a command in a finalize scope.
483pub fn cost_per_command<N: Network>(
484    stack: &Stack<N>,
485    finalize_types: &FinalizeTypes<N>,
486    command: &Command<N>,
487    consensus_fee_version: ConsensusFeeVersion,
488) -> Result<u64> {
489    let mapping_base_cost = match consensus_fee_version {
490        ConsensusFeeVersion::V1 => MAPPING_BASE_COST_V1,
491        ConsensusFeeVersion::V2 | ConsensusFeeVersion::V3 => MAPPING_BASE_COST_V2,
492    };
493
494    match command {
495        Command::Instruction(Instruction::Abs(_)) => Ok(500),
496        Command::Instruction(Instruction::AbsWrapped(_)) => Ok(500),
497        Command::Instruction(Instruction::Add(_)) => Ok(500),
498        Command::Instruction(Instruction::AddWrapped(_)) => Ok(500),
499        Command::Instruction(Instruction::And(_)) => Ok(500),
500        Command::Instruction(Instruction::AssertEq(_)) => Ok(500),
501        Command::Instruction(Instruction::AssertNeq(_)) => Ok(500),
502        Command::Instruction(Instruction::Async(_)) => bail!("'async' is not supported in finalize"),
503        Command::Instruction(Instruction::Call(_)) => bail!("'call' is not supported in finalize"),
504        Command::Instruction(Instruction::CallDynamic(_)) => {
505            bail!("'{}' is not supported in finalize", CallDynamic::<N>::opcode())
506        }
507        Command::Instruction(Instruction::Cast(cast)) => match cast.cast_type() {
508            CastType::Plaintext(PlaintextType::Literal(_)) => Ok(500),
509            CastType::Plaintext(plaintext_type) => Ok(plaintext_size_in_bytes(stack, plaintext_type)?
510                .saturating_mul(CAST_PER_BYTE_COST)
511                .saturating_add(CAST_BASE_COST)),
512            CastType::GroupXCoordinate | CastType::GroupYCoordinate => Ok(500),
513            CastType::Record(_) => bail!("'cast' to a record is not supported in finalize"),
514            CastType::ExternalRecord(_) => bail!("'cast' to an external record is not supported in finalize"),
515            CastType::DynamicRecord => bail!("'cast' to a dynamic record is not supported in finalize"),
516        },
517        Command::Instruction(Instruction::CastLossy(cast_lossy)) => match cast_lossy.cast_type() {
518            CastType::Plaintext(PlaintextType::Literal(_)) => Ok(500),
519            CastType::Plaintext(plaintext_type) => Ok(plaintext_size_in_bytes(stack, plaintext_type)?
520                .saturating_mul(CAST_PER_BYTE_COST)
521                .saturating_add(CAST_BASE_COST)),
522            CastType::GroupXCoordinate | CastType::GroupYCoordinate => Ok(500),
523            CastType::Record(_) => bail!("'cast.lossy' to a record is not supported in finalize"),
524            CastType::ExternalRecord(_) => bail!("'cast.lossy' to an external record is not supported in finalize"),
525            CastType::DynamicRecord => bail!("'cast.lossy' to a dynamic record is not supported in finalize"),
526        },
527        Command::Instruction(Instruction::CommitBHP256(commit)) => {
528            cost_in_size(stack, finalize_types, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
529        }
530        Command::Instruction(Instruction::CommitBHP512(commit)) => {
531            cost_in_size(stack, finalize_types, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
532        }
533        Command::Instruction(Instruction::CommitBHP768(commit)) => {
534            cost_in_size(stack, finalize_types, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
535        }
536        Command::Instruction(Instruction::CommitBHP1024(commit)) => {
537            cost_in_size(stack, finalize_types, commit.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
538        }
539        Command::Instruction(Instruction::CommitPED64(commit)) => {
540            cost_in_size(stack, finalize_types, commit.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
541        }
542        Command::Instruction(Instruction::CommitPED128(commit)) => {
543            cost_in_size(stack, finalize_types, commit.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
544        }
545        Command::Instruction(Instruction::DeserializeBits(deserialize)) => {
546            Ok(plaintext_size_in_bytes(stack, &PlaintextType::Array(deserialize.operand_type().clone()))?
547                .saturating_mul(CAST_PER_BYTE_COST)
548                .saturating_add(CAST_BASE_COST))
549        }
550        Command::Instruction(Instruction::DeserializeBitsRaw(deserialize)) => {
551            Ok(plaintext_size_in_bytes(stack, &PlaintextType::Array(deserialize.operand_type().clone()))?
552                .saturating_mul(CAST_PER_BYTE_COST)
553                .saturating_add(CAST_BASE_COST))
554        }
555        Command::Instruction(Instruction::Div(div)) => {
556            // Ensure `div` has exactly two operands.
557            ensure!(div.operands().len() == 2, "'div' must contain exactly 2 operands");
558            // Retrieve the price by the operand type.
559            match finalize_types.get_type_from_operand(stack, &div.operands()[0])? {
560                FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)) => Ok(1_500),
561                FinalizeType::Plaintext(PlaintextType::Literal(_)) => Ok(500),
562                FinalizeType::Plaintext(PlaintextType::Array(_)) => bail!("'div' does not support arrays"),
563                FinalizeType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => {
564                    bail!("'div' does not support structs")
565                }
566                FinalizeType::Future(_) => bail!("'div' does not support futures"),
567                FinalizeType::DynamicFuture => bail!("'div' does not support dynamic futures"),
568            }
569        }
570        Command::Instruction(Instruction::DivWrapped(_)) => Ok(500),
571        Command::Instruction(Instruction::Double(_)) => Ok(500),
572        Command::Instruction(Instruction::ECDSAVerifyDigest(_)) => Ok(ECDSA_VERIFY_BASE_COST),
573        Command::Instruction(Instruction::ECDSAVerifyDigestEth(_)) => Ok(ECDSA_VERIFY_ETH_BASE_COST),
574        Command::Instruction(Instruction::ECDSAVerifyKeccak256(ecdsa)) => {
575            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
576        }
577        Command::Instruction(Instruction::ECDSAVerifyKeccak256Raw(ecdsa)) => {
578            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
579        }
580        Command::Instruction(Instruction::ECDSAVerifyKeccak256Eth(ecdsa)) => {
581            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_ETH_BASE_COST)
582        }
583        Command::Instruction(Instruction::ECDSAVerifyKeccak384(ecdsa)) => {
584            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
585        }
586        Command::Instruction(Instruction::ECDSAVerifyKeccak384Raw(ecdsa)) => {
587            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
588        }
589        Command::Instruction(Instruction::ECDSAVerifyKeccak384Eth(ecdsa)) => {
590            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_ETH_BASE_COST)
591        }
592        Command::Instruction(Instruction::ECDSAVerifyKeccak512(ecdsa)) => {
593            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
594        }
595        Command::Instruction(Instruction::ECDSAVerifyKeccak512Raw(ecdsa)) => {
596            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
597        }
598        Command::Instruction(Instruction::ECDSAVerifyKeccak512Eth(ecdsa)) => {
599            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_ETH_BASE_COST)
600        }
601        Command::Instruction(Instruction::ECDSAVerifySha3_256(ecdsa)) => {
602            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
603        }
604        Command::Instruction(Instruction::ECDSAVerifySha3_256Raw(ecdsa)) => {
605            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
606        }
607        Command::Instruction(Instruction::ECDSAVerifySha3_256Eth(ecdsa)) => {
608            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_ETH_BASE_COST)
609        }
610        Command::Instruction(Instruction::ECDSAVerifySha3_384(ecdsa)) => {
611            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
612        }
613        Command::Instruction(Instruction::ECDSAVerifySha3_384Raw(ecdsa)) => {
614            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
615        }
616        Command::Instruction(Instruction::ECDSAVerifySha3_384Eth(ecdsa)) => {
617            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_ETH_BASE_COST)
618        }
619        Command::Instruction(Instruction::ECDSAVerifySha3_512(ecdsa)) => {
620            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
621        }
622        Command::Instruction(Instruction::ECDSAVerifySha3_512Raw(ecdsa)) => {
623            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_BASE_COST)
624        }
625        Command::Instruction(Instruction::ECDSAVerifySha3_512Eth(ecdsa)) => {
626            cost_in_size(stack, finalize_types, ecdsa.operands(), HASH_PER_BYTE_COST, ECDSA_VERIFY_ETH_BASE_COST)
627        }
628        Command::Instruction(Instruction::GetRecordDynamic(_)) => {
629            bail!("'{}' is not supported in finalize", GetRecordDynamic::<N>::opcode())
630        }
631        Command::Instruction(Instruction::GreaterThan(_)) => Ok(500),
632        Command::Instruction(Instruction::GreaterThanOrEqual(_)) => Ok(500),
633        Command::Instruction(Instruction::HashBHP256(hash)) => {
634            cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
635        }
636        Command::Instruction(Instruction::HashBHP256Raw(hash)) => {
637            cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
638        }
639        Command::Instruction(Instruction::HashBHP512(hash)) => {
640            cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
641        }
642        Command::Instruction(Instruction::HashBHP512Raw(hash)) => {
643            cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
644        }
645        Command::Instruction(Instruction::HashBHP768(hash)) => {
646            cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
647        }
648        Command::Instruction(Instruction::HashBHP768Raw(hash)) => {
649            cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
650        }
651        Command::Instruction(Instruction::HashBHP1024(hash)) => {
652            cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
653        }
654        Command::Instruction(Instruction::HashBHP1024Raw(hash)) => {
655            cost_in_size(stack, finalize_types, hash.operands(), HASH_BHP_PER_BYTE_COST, HASH_BHP_BASE_COST)
656        }
657        Command::Instruction(Instruction::HashKeccak256(hash)) => {
658            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
659        }
660        Command::Instruction(Instruction::HashKeccak256Raw(hash)) => {
661            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
662        }
663        Command::Instruction(Instruction::HashKeccak256Native(hash)) => {
664            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
665        }
666        Command::Instruction(Instruction::HashKeccak256NativeRaw(hash)) => {
667            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
668        }
669        Command::Instruction(Instruction::HashKeccak384(hash)) => {
670            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
671        }
672        Command::Instruction(Instruction::HashKeccak384Raw(hash)) => {
673            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
674        }
675        Command::Instruction(Instruction::HashKeccak384Native(hash)) => {
676            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
677        }
678        Command::Instruction(Instruction::HashKeccak384NativeRaw(hash)) => {
679            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
680        }
681        Command::Instruction(Instruction::HashKeccak512(hash)) => {
682            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
683        }
684        Command::Instruction(Instruction::HashKeccak512Raw(hash)) => {
685            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
686        }
687        Command::Instruction(Instruction::HashKeccak512Native(hash)) => {
688            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
689        }
690        Command::Instruction(Instruction::HashKeccak512NativeRaw(hash)) => {
691            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
692        }
693        Command::Instruction(Instruction::HashPED64(hash)) => {
694            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
695        }
696        Command::Instruction(Instruction::HashPED64Raw(hash)) => {
697            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
698        }
699        Command::Instruction(Instruction::HashPED128(hash)) => {
700            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
701        }
702        Command::Instruction(Instruction::HashPED128Raw(hash)) => {
703            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
704        }
705        Command::Instruction(Instruction::HashPSD2(hash)) => {
706            cost_in_size(stack, finalize_types, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST)
707        }
708        Command::Instruction(Instruction::HashPSD2Raw(hash)) => {
709            cost_in_size(stack, finalize_types, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST)
710        }
711        Command::Instruction(Instruction::HashPSD4(hash)) => {
712            cost_in_size(stack, finalize_types, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST)
713        }
714        Command::Instruction(Instruction::HashPSD4Raw(hash)) => {
715            cost_in_size(stack, finalize_types, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST)
716        }
717        Command::Instruction(Instruction::HashPSD8(hash)) => {
718            cost_in_size(stack, finalize_types, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST)
719        }
720        Command::Instruction(Instruction::HashPSD8Raw(hash)) => {
721            cost_in_size(stack, finalize_types, hash.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST)
722        }
723        Command::Instruction(Instruction::HashSha3_256(hash)) => {
724            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
725        }
726        Command::Instruction(Instruction::HashSha3_256Raw(hash)) => {
727            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
728        }
729        Command::Instruction(Instruction::HashSha3_256Native(hash)) => {
730            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
731        }
732        Command::Instruction(Instruction::HashSha3_256NativeRaw(hash)) => {
733            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
734        }
735        Command::Instruction(Instruction::HashSha3_384(hash)) => {
736            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
737        }
738        Command::Instruction(Instruction::HashSha3_384Raw(hash)) => {
739            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
740        }
741        Command::Instruction(Instruction::HashSha3_384Native(hash)) => {
742            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
743        }
744        Command::Instruction(Instruction::HashSha3_384NativeRaw(hash)) => {
745            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
746        }
747        Command::Instruction(Instruction::HashSha3_512(hash)) => {
748            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
749        }
750        Command::Instruction(Instruction::HashSha3_512Raw(hash)) => {
751            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
752        }
753        Command::Instruction(Instruction::HashSha3_512Native(hash)) => {
754            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
755        }
756        Command::Instruction(Instruction::HashSha3_512NativeRaw(hash)) => {
757            cost_in_size(stack, finalize_types, hash.operands(), HASH_PER_BYTE_COST, HASH_BASE_COST)
758        }
759        Command::Instruction(Instruction::HashManyPSD2(_)) => {
760            bail!("`hash_many.psd2` is not supported in finalize")
761        }
762        Command::Instruction(Instruction::HashManyPSD4(_)) => {
763            bail!("`hash_many.psd4` is not supported in finalize")
764        }
765        Command::Instruction(Instruction::HashManyPSD8(_)) => {
766            bail!("`hash_many.psd8` is not supported in finalize")
767        }
768        Command::Instruction(Instruction::Inv(_)) => Ok(2_500),
769        Command::Instruction(Instruction::IsEq(_)) => Ok(500),
770        Command::Instruction(Instruction::IsNeq(_)) => Ok(500),
771        Command::Instruction(Instruction::LessThan(_)) => Ok(500),
772        Command::Instruction(Instruction::LessThanOrEqual(_)) => Ok(500),
773        Command::Instruction(Instruction::Modulo(_)) => Ok(500),
774        Command::Instruction(Instruction::Mul(mul)) => {
775            // Ensure `mul` has exactly two operands.
776            ensure!(mul.operands().len() == 2, "'mul' must contain exactly 2 operands");
777            // Retrieve the price by operand type.
778            match finalize_types.get_type_from_operand(stack, &mul.operands()[0])? {
779                FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Group)) => Ok(10_000),
780                FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Scalar)) => Ok(10_000),
781                FinalizeType::Plaintext(PlaintextType::Literal(_)) => Ok(500),
782                FinalizeType::Plaintext(PlaintextType::Array(_)) => bail!("'mul' does not support arrays"),
783                FinalizeType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => {
784                    bail!("'mul' does not support structs")
785                }
786                FinalizeType::Future(_) => bail!("'mul' does not support futures"),
787                FinalizeType::DynamicFuture => bail!("'mul' does not support dynamic futures"),
788            }
789        }
790        Command::Instruction(Instruction::MulWrapped(_)) => Ok(500),
791        Command::Instruction(Instruction::Nand(_)) => Ok(500),
792        Command::Instruction(Instruction::Neg(_)) => Ok(500),
793        Command::Instruction(Instruction::Nor(_)) => Ok(500),
794        Command::Instruction(Instruction::Not(_)) => Ok(500),
795        Command::Instruction(Instruction::Or(_)) => Ok(500),
796        Command::Instruction(Instruction::Pow(pow)) => {
797            // Ensure `pow` has at least one operand.
798            ensure!(!pow.operands().is_empty(), "'pow' must contain at least 1 operand");
799            // Retrieve the price by operand type.
800            match finalize_types.get_type_from_operand(stack, &pow.operands()[0])? {
801                FinalizeType::Plaintext(PlaintextType::Literal(LiteralType::Field)) => Ok(1_500),
802                FinalizeType::Plaintext(PlaintextType::Literal(_)) => Ok(500),
803                FinalizeType::Plaintext(PlaintextType::Array(_)) => bail!("'pow' does not support arrays"),
804                FinalizeType::Plaintext(PlaintextType::Struct(_) | PlaintextType::ExternalStruct(_)) => {
805                    bail!("'pow' does not support structs")
806                }
807                FinalizeType::Future(_) => bail!("'pow' does not support futures"),
808                FinalizeType::DynamicFuture => bail!("'pow' does not support dynamic futures"),
809            }
810        }
811        Command::Instruction(Instruction::PowWrapped(_)) => Ok(500),
812        Command::Instruction(Instruction::Rem(_)) => Ok(500),
813        Command::Instruction(Instruction::RemWrapped(_)) => Ok(500),
814        Command::Instruction(Instruction::SerializeBits(serialize)) => {
815            Ok(plaintext_size_in_bytes(stack, &PlaintextType::Array(serialize.destination_type().clone()))?
816                .saturating_mul(CAST_PER_BYTE_COST)
817                .saturating_add(CAST_BASE_COST))
818        }
819        Command::Instruction(Instruction::SerializeBitsRaw(serialize)) => {
820            Ok(plaintext_size_in_bytes(stack, &PlaintextType::Array(serialize.destination_type().clone()))?
821                .saturating_mul(CAST_PER_BYTE_COST)
822                .saturating_add(CAST_BASE_COST))
823        }
824        Command::Instruction(Instruction::SignVerify(sign)) => {
825            cost_in_size(stack, finalize_types, sign.operands(), HASH_PSD_PER_BYTE_COST, HASH_PSD_BASE_COST)
826        }
827        Command::Instruction(Instruction::Shl(_)) => Ok(500),
828        Command::Instruction(Instruction::ShlWrapped(_)) => Ok(500),
829        Command::Instruction(Instruction::Shr(_)) => Ok(500),
830        Command::Instruction(Instruction::ShrWrapped(_)) => Ok(500),
831        Command::Instruction(Instruction::SnarkVerify(snark)) => {
832            cost_in_size(stack, finalize_types, snark.operands(), SNARK_VERIFY_PER_BYTE_COST, SNARK_VERIFY_BASE_COST)
833        }
834        Command::Instruction(Instruction::SnarkVerifyBatch(snark)) => {
835            cost_in_size(stack, finalize_types, snark.operands(), SNARK_VERIFY_PER_BYTE_COST, SNARK_VERIFY_BASE_COST)
836        }
837        Command::Instruction(Instruction::Square(_)) => Ok(500),
838        Command::Instruction(Instruction::SquareRoot(_)) => Ok(2_500),
839        Command::Instruction(Instruction::Sub(_)) => Ok(500),
840        Command::Instruction(Instruction::SubWrapped(_)) => Ok(500),
841        Command::Instruction(Instruction::Ternary(_)) => Ok(500),
842        Command::Instruction(Instruction::Xor(_)) => Ok(500),
843        Command::Await(_) => Ok(500),
844        Command::Contains(command) => {
845            cost_in_size(stack, finalize_types, [command.key()], MAPPING_PER_BYTE_COST, mapping_base_cost)
846        }
847        Command::ContainsDynamic(command) => {
848            cost_in_size(stack, finalize_types, [command.key_operand()], MAPPING_PER_BYTE_COST, mapping_base_cost)
849        }
850        Command::Get(command) => {
851            cost_in_size(stack, finalize_types, [command.key()], MAPPING_PER_BYTE_COST, mapping_base_cost)
852        }
853        Command::GetDynamic(command) => {
854            cost_in_size(stack, finalize_types, [command.key_operand()], MAPPING_PER_BYTE_COST, mapping_base_cost)
855        }
856        Command::GetOrUse(command) => {
857            cost_in_size(stack, finalize_types, [command.key()], MAPPING_PER_BYTE_COST, mapping_base_cost)
858        }
859        Command::GetOrUseDynamic(command) => {
860            cost_in_size(stack, finalize_types, [command.key_operand()], MAPPING_PER_BYTE_COST, mapping_base_cost)
861        }
862        Command::RandChaCha(_) => Ok(25_000),
863        Command::Remove(_) => Ok(SET_BASE_COST),
864        Command::Set(command) => {
865            cost_in_size(stack, finalize_types, [command.key(), command.value()], SET_PER_BYTE_COST, SET_BASE_COST)
866        }
867        Command::BranchEq(_) | Command::BranchNeq(_) => Ok(500),
868        Command::Position(_) => Ok(100),
869    }
870}
871
872/// Returns the minimum number of microcredits required to run the constructor in the given stack.
873/// If a constructor does not exist, no cost is incurred.
874pub fn constructor_cost_in_microcredits_v2<N: Network>(stack: &Stack<N>) -> Result<u64> {
875    match stack.program().constructor() {
876        Some(constructor) => {
877            // Get the constructor types.
878            let constructor_types = stack.get_constructor_types()?;
879            // Get the base cost of the constructor.
880            let base_cost = constructor
881                .commands()
882                .iter()
883                .map(|command| cost_per_command(stack, &constructor_types, command, ConsensusFeeVersion::V2))
884                .try_fold(0u64, |acc, res| {
885                    res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Constructor cost overflowed")))
886                })?;
887            // Scale by the multiplier and divide by the ARC-0005 cost reduction factor.
888            base_cost
889                .checked_mul(N::CONSTRUCTOR_FEE_MULTIPLIER)
890                .map(|result| result / N::ARC_0005_COMPUTE_DISCOUNT)
891                .ok_or(anyhow!("Constructor cost overflowed"))
892        }
893        None => Ok(0),
894    }
895}
896
897/// Returns the minimum number of microcredits required to run the constructor in the given stack.
898/// If a constructor does not exist, no cost is incurred.
899pub fn constructor_cost_in_microcredits_v1<N: Network>(stack: &Stack<N>) -> Result<u64> {
900    match stack.program().constructor() {
901        Some(constructor) => {
902            // Get the constructor types.
903            let constructor_types = stack.get_constructor_types()?;
904            // Get the base cost of the constructor.
905            let base_cost = constructor
906                .commands()
907                .iter()
908                .map(|command| cost_per_command(stack, &constructor_types, command, ConsensusFeeVersion::V2))
909                .try_fold(0u64, |acc, res| {
910                    res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Constructor cost overflowed")))
911                })?;
912            // Scale by the multiplier and divide by the ARC-0005 cost reduction factor.
913            base_cost.checked_mul(N::CONSTRUCTOR_FEE_MULTIPLIER).ok_or(anyhow!("Constructor cost overflowed"))
914        }
915        None => Ok(0),
916    }
917}
918
919/// Returns the minimum number of microcredits required to run the finalize using the ARC-0005 cost reduction factor.
920/// Note: For dynamic futures, this only provides a lower bound on the cost because the target functions
921/// cannot be statically determined. For exact execution cost, use `execution_finalize_cost`.
922pub fn minimum_cost_in_microcredits_v3<N: Network>(stack: &Stack<N>, function_name: &Identifier<N>) -> Result<u64> {
923    minimum_cost_in_microcredits(stack, function_name, ConsensusFeeVersion::V3)
924}
925
926/// Returns the finalize cost for a single function's finalize block, without recursively following futures.
927/// This is used for runtime cost calculation where we iterate over concrete transitions.
928/// Note: This returns the RAW cost without applying the quotient divisor. The caller is responsible
929/// for applying the quotient after summing all costs to avoid integer division truncation errors.
930fn finalize_cost_for_single_function_raw<N: Network>(
931    stack: &Stack<N>,
932    function_name: &Identifier<N>,
933    consensus_fee_version: ConsensusFeeVersion,
934) -> Result<u64> {
935    // Get the finalize logic. If the function does not have a finalize scope, no cost is incurred.
936    let Some(finalize) = stack.get_function_ref(function_name)?.finalize_logic() else {
937        return Ok(0);
938    };
939
940    // Get the finalize types.
941    let finalize_types = stack.get_finalize_types(finalize.name())?;
942
943    // Sum the cost of all commands in the finalize block.
944    // Note: We don't recursively follow futures here because each transition's
945    // finalize cost is computed separately when iterating over all transitions.
946    let mut finalize_cost = 0u64;
947    for command in finalize.commands() {
948        finalize_cost = finalize_cost
949            .checked_add(cost_per_command(stack, &finalize_types, command, consensus_fee_version)?)
950            .ok_or(anyhow!("Finalize cost overflowed"))?;
951    }
952
953    Ok(finalize_cost)
954}
955
956/// Returns the total finalize cost for an execution by iterating over all concrete transitions.
957/// This gives an exact cost calculation because we know which functions were actually called.
958/// The complexity is O(MAX_TRANSITIONS * MAX_COMMANDS_PER_FINALIZE) which is bounded.
959pub(crate) fn execution_finalize_cost<N: Network>(
960    process: &Process<N>,
961    execution: &Execution<N>,
962    consensus_fee_version: ConsensusFeeVersion,
963) -> Result<u64> {
964    // Get the quotient for the cost reduction factor.
965    // We apply this at the end after summing all costs to match the behavior of
966    // minimum_cost_in_microcredits and avoid integer division truncation errors.
967    let quotient = match consensus_fee_version {
968        ConsensusFeeVersion::V1 | ConsensusFeeVersion::V2 => 1,
969        ConsensusFeeVersion::V3 => N::ARC_0005_COMPUTE_DISCOUNT,
970    };
971
972    let mut total_cost = 0u64;
973
974    // Iterate over all transitions and sum their finalize costs.
975    // This is bounded by Transaction::MAX_TRANSITIONS.
976    for transition in execution.transitions() {
977        // Get the stack for this transition's program.
978        let stack = process.get_stack(transition.program_id())?;
979        // Compute the raw finalize cost for this single transition (without quotient).
980        let cost = finalize_cost_for_single_function_raw(&stack, transition.function_name(), consensus_fee_version)?;
981        // Add to the total.
982        total_cost = total_cost.checked_add(cost).ok_or(anyhow!("Execution finalize cost overflowed"))?;
983    }
984
985    // Apply the quotient divisor at the end (matching behavior of minimum_cost_in_microcredits).
986    Ok(total_cost / quotient)
987}
988
989/// Returns the minimum number of microcredits required to run the finalize.
990/// Note: For dynamic futures, this only provides a lower bound on the cost because the target functions
991/// cannot be statically determined. For exact execution cost, use `execution_finalize_cost`.
992pub fn minimum_cost_in_microcredits_v2<N: Network>(stack: &Stack<N>, function_name: &Identifier<N>) -> Result<u64> {
993    minimum_cost_in_microcredits(stack, function_name, ConsensusFeeVersion::V2)
994}
995
996/// Returns the minimum number of microcredits required to run the finalize (deprecated).
997/// Note: For dynamic futures, this only provides a lower bound on the cost because the target functions
998/// cannot be statically determined. For exact execution cost, use `execution_finalize_cost`.
999pub fn minimum_cost_in_microcredits_v1<N: Network>(stack: &Stack<N>, function_name: &Identifier<N>) -> Result<u64> {
1000    minimum_cost_in_microcredits(stack, function_name, ConsensusFeeVersion::V1)
1001}
1002
1003// A helper function to compute the minimum cost in microcredits for a given function.
1004// This performs static analysis and recursively follows static futures, but not dynamic futures.
1005fn minimum_cost_in_microcredits<N: Network>(
1006    stack: &Stack<N>,
1007    function_name: &Identifier<N>,
1008    consensus_fee_version: ConsensusFeeVersion,
1009) -> Result<u64> {
1010    // Initialize the base cost.
1011    let mut finalize_cost = 0u64;
1012    // Initialize a queue of finalize blocks to tally.
1013    let mut finalizes = vec![(StackRef::Internal(stack), *function_name)];
1014    // Initialize a counter for the number of finalize blocks seen.
1015    let mut num_finalizes = 1;
1016    // Get the quotient for the cost reduction factor.
1017    let quotient = match consensus_fee_version {
1018        ConsensusFeeVersion::V1 | ConsensusFeeVersion::V2 => 1,
1019        ConsensusFeeVersion::V3 => N::ARC_0005_COMPUTE_DISCOUNT,
1020    };
1021    // Iterate over the finalize blocks.
1022    while let Some((stack_ref, function_name)) = finalizes.pop() {
1023        // Ensure that the number of finalize blocks does not exceed the maximum.
1024        // Note that one transition is reserved for the fee.
1025        ensure!(
1026            num_finalizes < Transaction::<N>::MAX_TRANSITIONS,
1027            "The number of finalize blocks must be less than '{}'",
1028            Transaction::<N>::MAX_TRANSITIONS
1029        );
1030        // Get the finalize logic. If the function does not have a finalize scope then no cost is incurred.
1031        // Note: For dynamic futures, this only creates a lower bound on the cost because we cannot
1032        // statically determine which functions will be called. For exact execution cost calculation,
1033        // use `execution_finalize_cost` which iterates over concrete transitions.
1034        if let Some(finalize) = stack_ref.get_function_ref(&function_name)?.finalize_logic() {
1035            // Queue the futures to be tallied.
1036            for input in finalize.inputs() {
1037                if let FinalizeType::Future(future) = input.finalize_type() {
1038                    // Increment the number of finalize blocks seen.
1039                    num_finalizes += 1;
1040                    // If the locator matches the program ID of the provided stack, use it directly.
1041                    // Otherwise, retrieve the external stack.
1042                    let stack = if future.program_id() == stack.program().id() {
1043                        StackRef::Internal(stack)
1044                    } else {
1045                        StackRef::External(stack_ref.get_external_stack(future.program_id())?)
1046                    };
1047                    // Queue the future.
1048                    finalizes.push((stack, *future.resource()));
1049                }
1050            }
1051            // Get the finalize types.
1052            let finalize_types = stack_ref.get_finalize_types(finalize.name())?;
1053            // Iterate over the commands in the finalize block.
1054            for command in finalize.commands() {
1055                // Sum the cost of all commands in the current future into the total running cost.
1056                finalize_cost = finalize_cost
1057                    .checked_add(cost_per_command(&stack_ref, &finalize_types, command, consensus_fee_version)?)
1058                    .ok_or(anyhow!("Finalize cost overflowed"))?;
1059            }
1060        }
1061    }
1062    Ok(finalize_cost / quotient)
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067    use super::*;
1068    use crate::test_helpers::get_execution;
1069    use circuit::{Aleo, AleoCanaryV0, AleoTestnetV0, AleoV0};
1070
1071    use console::{
1072        network::{CanaryV0, ConsensusVersion, MainnetV0, TestnetV0, prelude::consensus_config_value_by_version},
1073        types::Address,
1074    };
1075    use snarkvm_synthesizer_program::Program;
1076
1077    // Test program with two functions just below and above the size threshold.
1078    const SIZE_BOUNDARY_PROGRAM: &str = r#"
1079program size_boundary.aleo;
1080
1081function under_five_thousand:
1082    input r0 as group.public;
1083    cast r0 r0 r0 r0 r0 r0 r0 r0 r0 into r1 as [group; 9u32];
1084    cast r1 r1 r1 r1 r1 r1 r1 r1 r1 r1 into r2 as [[group; 9u32]; 10u32];
1085    cast r0 r0 r0 r0 r0 r0 r0 into r3 as [group; 7u32];
1086    output r2 as [[group; 9u32]; 10u32].public;
1087    output r3 as [group; 7u32].public;
1088
1089function over_five_thousand:
1090    input r0 as group.public;
1091    cast r0 r0 r0 r0 r0 r0 r0 r0 r0 into r1 as [group; 9u32];
1092    cast r1 r1 r1 r1 r1 r1 r1 r1 r1 r1 into r2 as [[group; 9u32]; 10u32];
1093    cast r0 r0 r0 r0 r0 r0 r0 into r3 as [group; 7u32];
1094    output r2 as [[group; 9u32]; 10u32].public;
1095    output r3 as [group; 7u32].public;
1096    output 5u64 as u64.public;
1097    "#;
1098    // Cost for a program +1 byte above the threshold.
1099    const STORAGE_COST_ABOVE_THRESHOLD: u64 = 5002;
1100    // Storage cost for an execution transaction at the maximum transaction size.
1101    const V1_STORAGE_COST_MAX: u64 = 3_276_800;
1102    const V14_STORAGE_COST_MAX: u64 = 117_964_800;
1103
1104    fn test_storage_cost_bounds<N: Network>() {
1105        // Calculate the bounds directly above and below the size threshold.
1106        let threshold = N::EXECUTION_STORAGE_PENALTY_THRESHOLD;
1107        let threshold_lower_offset = threshold.saturating_sub(1);
1108        let threshold_upper_offset = threshold.saturating_add(1);
1109
1110        // Test the storage cost bounds.
1111        assert_eq!(execution_storage_cost::<N>(0), 0);
1112        assert_eq!(execution_storage_cost::<N>(1), 1);
1113        assert_eq!(execution_storage_cost::<N>(threshold_lower_offset), threshold_lower_offset);
1114        assert_eq!(execution_storage_cost::<N>(threshold), threshold);
1115        assert_eq!(execution_storage_cost::<N>(threshold_upper_offset), STORAGE_COST_ABOVE_THRESHOLD);
1116        // Use V1 value for backwards-compatible test.
1117        let v1_max_tx_size = consensus_config_value_by_version!(N, MAX_TRANSACTION_SIZE, ConsensusVersion::V1).unwrap();
1118        assert_eq!(execution_storage_cost::<N>(v1_max_tx_size as u64), V1_STORAGE_COST_MAX);
1119        let v14_max_tx_size =
1120            consensus_config_value_by_version!(N, MAX_TRANSACTION_SIZE, ConsensusVersion::V14).unwrap();
1121        assert_eq!(execution_storage_cost::<N>(v14_max_tx_size as u64), V14_STORAGE_COST_MAX);
1122    }
1123
1124    #[test]
1125    fn test_storage_cost_bounds_for_all_networks() {
1126        test_storage_cost_bounds::<CanaryV0>();
1127        test_storage_cost_bounds::<MainnetV0>();
1128        test_storage_cost_bounds::<TestnetV0>();
1129    }
1130
1131    #[test]
1132    fn test_storage_costs_compute_correctly() {
1133        // Test the storage cost of an execution.
1134        let threshold = MainnetV0::EXECUTION_STORAGE_PENALTY_THRESHOLD;
1135
1136        // Test the cost of an execution.
1137        let mut process = Process::load().unwrap();
1138
1139        // Get the program.
1140        let program = Program::from_str(SIZE_BOUNDARY_PROGRAM).unwrap();
1141
1142        // Get the program identifiers.
1143        let under_5000 = Identifier::from_str("under_five_thousand").unwrap();
1144        let over_5000 = Identifier::from_str("over_five_thousand").unwrap();
1145
1146        // Get execution and cost data.
1147        let execution_under_5000 = get_execution(&mut process, &program, &under_5000, ["2group"].into_iter());
1148        let execution_size_under_5000 = execution_under_5000.size_in_bytes().unwrap();
1149        let (_, (storage_cost_under_5000, _)) =
1150            execution_cost_v3(&process, &execution_under_5000, execution_size_under_5000).unwrap();
1151        let execution_over_5000 = get_execution(&mut process, &program, &over_5000, ["2group"].into_iter());
1152        let execution_size_over_5000 = execution_over_5000.size_in_bytes().unwrap();
1153        let (_, (storage_cost_over_5000, _)) =
1154            execution_cost_v3(&process, &execution_over_5000, execution_size_over_5000).unwrap();
1155
1156        // Ensure the sizes are below and above the threshold respectively.
1157        assert!(execution_size_under_5000 < threshold);
1158        assert!(execution_size_over_5000 > threshold);
1159
1160        // Ensure storage costs compute correctly.
1161        assert_eq!(storage_cost_under_5000, execution_storage_cost::<MainnetV0>(execution_size_under_5000));
1162        assert_eq!(storage_cost_over_5000, execution_storage_cost::<MainnetV0>(execution_size_over_5000));
1163    }
1164
1165    #[test]
1166    fn test_deployment_cost_with_constructors() {
1167        // A helper to run the test.
1168        fn run_test<N: Network, A: Aleo<Network = N>>() {
1169            let process = Process::<N>::load().unwrap();
1170            let rng = &mut TestRng::default();
1171
1172            // Define the programs.
1173            let program_0 = Program::from_str(
1174                r"
1175program program_with_constructor.aleo;
1176
1177constructor:
1178    assert.eq true true;
1179
1180mapping foo:
1181    key as field.public;
1182    value as field.public;
1183
1184function dummy:",
1185            )
1186            .unwrap();
1187
1188            let program_1 = Program::from_str(
1189                r"
1190program program_with_constructor.aleo;
1191
1192constructor:
1193    assert.eq edition 0u16;
1194
1195mapping foo:
1196    key as field.public;
1197    value as field.public;
1198
1199function dummy:",
1200            )
1201            .unwrap();
1202
1203            let program_2 = Program::from_str(
1204                r"
1205program program_with_constructor.aleo;
1206
1207constructor:
1208    get foo[0field] into r0;
1209
1210mapping foo:
1211    key as field.public;
1212    value as field.public;
1213
1214function dummy:",
1215            )
1216            .unwrap();
1217
1218            let program_3 = Program::from_str(
1219                r"
1220program program_with_constructor.aleo;
1221
1222constructor:
1223    set 0field into foo[0field];
1224
1225mapping foo:
1226    key as field.public;
1227    value as field.public;
1228
1229function dummy:",
1230            )
1231            .unwrap();
1232
1233            // Verify the deployment costs.
1234            let mut deployment_0 = process.deploy::<A, _>(&program_0, rng).unwrap();
1235            deployment_0.set_program_checksum_raw(Some(deployment_0.program().to_checksum()));
1236            deployment_0.set_program_owner_raw(Some(Address::rand(rng)));
1237            let expected_storage_cost = 879000;
1238            let expected_synthesis_cost = 603500;
1239            let expected_constructor_cost = 50000;
1240            let expected_namespace_cost = 1000000;
1241            let expected_total_cost =
1242                expected_storage_cost + expected_synthesis_cost + expected_constructor_cost + expected_namespace_cost;
1243            assert_eq!(
1244                deployment_cost_v1(&process, &deployment_0).unwrap(),
1245                (
1246                    expected_total_cost,
1247                    (
1248                        expected_storage_cost,
1249                        expected_synthesis_cost,
1250                        expected_constructor_cost,
1251                        expected_namespace_cost
1252                    )
1253                )
1254            );
1255            let expected_synthesis_cost = expected_synthesis_cost / N::ARC_0005_COMPUTE_DISCOUNT;
1256            let expected_constructor_cost = expected_constructor_cost / N::ARC_0005_COMPUTE_DISCOUNT;
1257            let expected_total_cost =
1258                expected_storage_cost + expected_synthesis_cost + expected_constructor_cost + expected_namespace_cost;
1259            assert_eq!(
1260                deployment_cost_v2(&process, &deployment_0).unwrap(),
1261                (
1262                    expected_total_cost,
1263                    (
1264                        expected_storage_cost,
1265                        expected_synthesis_cost,
1266                        expected_constructor_cost,
1267                        expected_namespace_cost
1268                    )
1269                )
1270            );
1271
1272            let mut deployment_1 = process.deploy::<A, _>(&program_1, rng).unwrap();
1273            deployment_1.set_program_checksum_raw(Some(deployment_1.program().to_checksum()));
1274            deployment_1.set_program_owner_raw(Some(Address::rand(rng)));
1275            let expected_storage_cost = 878000;
1276            let expected_synthesis_cost = 603500;
1277            let expected_constructor_cost = 50000;
1278            let expected_namespace_cost = 1000000;
1279            let expected_total_cost =
1280                expected_storage_cost + expected_synthesis_cost + expected_constructor_cost + expected_namespace_cost;
1281            assert_eq!(
1282                deployment_cost_v1(&process, &deployment_1).unwrap(),
1283                (
1284                    expected_total_cost,
1285                    (
1286                        expected_storage_cost,
1287                        expected_synthesis_cost,
1288                        expected_constructor_cost,
1289                        expected_namespace_cost
1290                    )
1291                )
1292            );
1293            let expected_synthesis_cost = expected_synthesis_cost / N::ARC_0005_COMPUTE_DISCOUNT;
1294            let expected_constructor_cost = expected_constructor_cost / N::ARC_0005_COMPUTE_DISCOUNT;
1295            let expected_total_cost =
1296                expected_storage_cost + expected_synthesis_cost + expected_constructor_cost + expected_namespace_cost;
1297            assert_eq!(
1298                deployment_cost_v2(&process, &deployment_1).unwrap(),
1299                (
1300                    expected_total_cost,
1301                    (
1302                        expected_storage_cost,
1303                        expected_synthesis_cost,
1304                        expected_constructor_cost,
1305                        expected_namespace_cost
1306                    )
1307                )
1308            );
1309
1310            let mut deployment_2 = process.deploy::<A, _>(&program_2, rng).unwrap();
1311            deployment_2.set_program_checksum_raw(Some(deployment_2.program().to_checksum()));
1312            deployment_2.set_program_owner_raw(Some(Address::rand(rng)));
1313            let expected_storage_cost = 911000;
1314            let expected_synthesis_cost = 603500;
1315            let expected_constructor_cost = 182000;
1316            let expected_namespace_cost = 1000000;
1317            let expected_total_cost =
1318                expected_storage_cost + expected_synthesis_cost + expected_constructor_cost + expected_namespace_cost;
1319            assert_eq!(
1320                deployment_cost_v1(&process, &deployment_2).unwrap(),
1321                (
1322                    expected_total_cost,
1323                    (
1324                        expected_storage_cost,
1325                        expected_synthesis_cost,
1326                        expected_constructor_cost,
1327                        expected_namespace_cost
1328                    )
1329                )
1330            );
1331            let expected_synthesis_cost = expected_synthesis_cost / N::ARC_0005_COMPUTE_DISCOUNT;
1332            let expected_constructor_cost = expected_constructor_cost / N::ARC_0005_COMPUTE_DISCOUNT;
1333            let expected_total_cost =
1334                expected_storage_cost + expected_synthesis_cost + expected_constructor_cost + expected_namespace_cost;
1335            assert_eq!(
1336                deployment_cost_v2(&process, &deployment_2).unwrap(),
1337                (
1338                    expected_total_cost,
1339                    (
1340                        expected_storage_cost,
1341                        expected_synthesis_cost,
1342                        expected_constructor_cost,
1343                        expected_namespace_cost
1344                    )
1345                )
1346            );
1347
1348            let mut deployment_3 = process.deploy::<A, _>(&program_3, rng).unwrap();
1349            deployment_3.set_program_checksum_raw(Some(deployment_3.program().to_checksum()));
1350            deployment_3.set_program_owner_raw(Some(Address::rand(rng)));
1351            let expected_storage_cost = 943000;
1352            let expected_synthesis_cost = 603500;
1353            let expected_constructor_cost = 1640000;
1354            let expected_namespace_cost = 1000000;
1355            let expected_total_cost =
1356                expected_storage_cost + expected_synthesis_cost + expected_constructor_cost + expected_namespace_cost;
1357            assert_eq!(
1358                deployment_cost_v1(&process, &deployment_3).unwrap(),
1359                (
1360                    expected_total_cost,
1361                    (
1362                        expected_storage_cost,
1363                        expected_synthesis_cost,
1364                        expected_constructor_cost,
1365                        expected_namespace_cost
1366                    )
1367                )
1368            );
1369            let expected_synthesis_cost = expected_synthesis_cost / N::ARC_0005_COMPUTE_DISCOUNT;
1370            let expected_constructor_cost = expected_constructor_cost / N::ARC_0005_COMPUTE_DISCOUNT;
1371            let expected_total_cost =
1372                expected_storage_cost + expected_synthesis_cost + expected_constructor_cost + expected_namespace_cost;
1373            assert_eq!(
1374                deployment_cost_v2(&process, &deployment_3).unwrap(),
1375                (
1376                    expected_total_cost,
1377                    (
1378                        expected_storage_cost,
1379                        expected_synthesis_cost,
1380                        expected_constructor_cost,
1381                        expected_namespace_cost
1382                    )
1383                )
1384            );
1385        }
1386
1387        // Run the tests for all networks.
1388        run_test::<CanaryV0, AleoCanaryV0>();
1389        run_test::<MainnetV0, AleoV0>();
1390        run_test::<TestnetV0, AleoTestnetV0>();
1391    }
1392
1393    // Test program with finalize blocks for cost comparison test
1394    const FINALIZE_PROGRAM: &str = r#"
1395program finalize_test.aleo;
1396
1397mapping counter:
1398    key as u64.public;
1399    value as u64.public;
1400
1401function increment:
1402    input r0 as u64.public;
1403    async increment r0 into r1;
1404    output r1 as finalize_test.aleo/increment.future;
1405
1406finalize increment:
1407    input r0 as u64.public;
1408    get.or_use counter[r0] 0u64 into r1;
1409    add r1 1u64 into r2;
1410    set r2 into counter[r0];
1411"#;
1412
1413    #[test]
1414    fn test_execution_finalize_cost_matches_static() {
1415        // This test verifies that for executions WITHOUT dynamic futures,
1416        // the runtime cost calculation (execution_finalize_cost) matches
1417        // the static cost calculation (minimum_cost_in_microcredits).
1418
1419        let mut process = Process::load().unwrap();
1420        let program = Program::from_str(FINALIZE_PROGRAM).unwrap();
1421        let function_name = Identifier::from_str("increment").unwrap();
1422
1423        // Get execution using the test helper
1424        let execution = get_execution(&mut process, &program, &function_name, ["42u64"].into_iter());
1425
1426        // Get the stack for static cost calculation
1427        let stack = process.get_stack(program.id()).unwrap();
1428
1429        // Calculate costs using both methods for all fee versions
1430        // V1
1431        let static_cost_v1 = minimum_cost_in_microcredits_v1(&stack, &function_name).unwrap();
1432        let runtime_cost_v1 = execution_finalize_cost(&process, &execution, ConsensusFeeVersion::V1).unwrap();
1433        assert_eq!(
1434            static_cost_v1, runtime_cost_v1,
1435            "V1: Static and runtime costs should match: static={static_cost_v1}, runtime={runtime_cost_v1}"
1436        );
1437
1438        // V2
1439        let static_cost_v2 = minimum_cost_in_microcredits_v2(&stack, &function_name).unwrap();
1440        let runtime_cost_v2 = execution_finalize_cost(&process, &execution, ConsensusFeeVersion::V2).unwrap();
1441        assert_eq!(
1442            static_cost_v2, runtime_cost_v2,
1443            "V2: Static and runtime costs should match: static={static_cost_v2}, runtime={runtime_cost_v2}"
1444        );
1445
1446        // V3
1447        let static_cost_v3 = minimum_cost_in_microcredits_v3(&stack, &function_name).unwrap();
1448        let runtime_cost_v3 = execution_finalize_cost(&process, &execution, ConsensusFeeVersion::V3).unwrap();
1449        assert_eq!(
1450            static_cost_v3, runtime_cost_v3,
1451            "V3: Static and runtime costs should match: static={static_cost_v3}, runtime={runtime_cost_v3}"
1452        );
1453
1454        // Verify costs are non-zero to ensure meaningful test
1455        assert!(static_cost_v1 > 0, "Expected non-zero cost");
1456        println!("Single function - Static cost V3: {static_cost_v3}");
1457    }
1458
1459    #[test]
1460    fn test_execution_finalize_cost_matches_static_nested_calls() {
1461        // This test verifies cost calculation with NESTED static futures.
1462        // Program structure:
1463        //   caller.aleo/call_child -> child.aleo/child_fn
1464        // Both have finalize blocks, and caller awaits child's future.
1465
1466        // Child program with a finalize block
1467        let (_, child_program) = Program::<MainnetV0>::parse(
1468            r"
1469program child.aleo;
1470
1471mapping child_counter:
1472    key as u64.public;
1473    value as u64.public;
1474
1475function child_fn:
1476    input r0 as u64.public;
1477    async child_fn r0 into r1;
1478    output r1 as child.aleo/child_fn.future;
1479
1480finalize child_fn:
1481    input r0 as u64.public;
1482    get.or_use child_counter[r0] 0u64 into r1;
1483    add r1 1u64 into r2;
1484    set r2 into child_counter[r0];
1485",
1486        )
1487        .unwrap();
1488
1489        // Caller program that calls child and awaits its future
1490        let (_, caller_program) = Program::<MainnetV0>::parse(
1491            r"
1492import child.aleo;
1493
1494program caller.aleo;
1495
1496mapping caller_counter:
1497    key as u64.public;
1498    value as u64.public;
1499
1500function call_child:
1501    input r0 as u64.public;
1502    call child.aleo/child_fn r0 into r1;
1503    async call_child r1 r0 into r2;
1504    output r2 as caller.aleo/call_child.future;
1505
1506finalize call_child:
1507    input r0 as child.aleo/child_fn.future;
1508    input r1 as u64.public;
1509    await r0;
1510    get.or_use caller_counter[r1] 0u64 into r2;
1511    add r2 10u64 into r3;
1512    set r3 into caller_counter[r1];
1513",
1514        )
1515        .unwrap();
1516
1517        // Build the process with both programs
1518        let mut process = crate::test_helpers::sample_process(&child_program);
1519        process.add_program(&caller_program).unwrap();
1520
1521        let function_name = Identifier::from_str("call_child").unwrap();
1522
1523        // Get execution
1524        let execution = get_execution(&mut process, &caller_program, &function_name, ["42u64"].into_iter());
1525
1526        // Verify we have 2 transitions (child + caller)
1527        assert_eq!(execution.transitions().count(), 2, "Expected 2 transitions for nested call");
1528
1529        // Get the stack for static cost calculation
1530        let stack = process.get_stack(caller_program.id()).unwrap();
1531
1532        // Calculate costs using both methods
1533        let static_cost_v3 = minimum_cost_in_microcredits_v3(&stack, &function_name).unwrap();
1534        let runtime_cost_v3 = execution_finalize_cost(&process, &execution, ConsensusFeeVersion::V3).unwrap();
1535
1536        println!("Nested calls - Static cost V3: {static_cost_v3}");
1537        println!("Nested calls - Runtime cost V3: {runtime_cost_v3}");
1538        println!("Nested calls - Number of transitions: {}", execution.transitions().count());
1539
1540        assert_eq!(
1541            static_cost_v3, runtime_cost_v3,
1542            "V3: Static and runtime costs should match for nested calls: static={static_cost_v3}, runtime={runtime_cost_v3}"
1543        );
1544
1545        // Also verify V1 and V2
1546        let static_cost_v1 = minimum_cost_in_microcredits_v1(&stack, &function_name).unwrap();
1547        let runtime_cost_v1 = execution_finalize_cost(&process, &execution, ConsensusFeeVersion::V1).unwrap();
1548        assert_eq!(
1549            static_cost_v1, runtime_cost_v1,
1550            "V1: Static and runtime costs should match for nested calls: static={static_cost_v1}, runtime={runtime_cost_v1}"
1551        );
1552
1553        let static_cost_v2 = minimum_cost_in_microcredits_v2(&stack, &function_name).unwrap();
1554        let runtime_cost_v2 = execution_finalize_cost(&process, &execution, ConsensusFeeVersion::V2).unwrap();
1555        assert_eq!(
1556            static_cost_v2, runtime_cost_v2,
1557            "V2: Static and runtime costs should match for nested calls: static={static_cost_v2}, runtime={runtime_cost_v2}"
1558        );
1559
1560        // Verify costs are non-zero and nested cost > single cost
1561        assert!(static_cost_v3 > 0, "Expected non-zero cost for nested calls");
1562    }
1563
1564    #[test]
1565    fn test_execution_finalize_cost_matches_static_complex_call_graph() {
1566        // This test verifies cost calculation with a COMPLEX call graph:
1567        //
1568        // root.aleo/main
1569        //   -> level1_a.aleo/fn_a (has finalize with await)
1570        //        -> leaf.aleo/leaf_fn (has finalize)
1571        //   -> level1_b.aleo/fn_b (has finalize with await)
1572        //        -> leaf.aleo/leaf_fn (has finalize)
1573        //
1574        // Execution order: leaf, fn_a, leaf, fn_b, main
1575        // Total: 5 transitions, 5 finalize blocks
1576
1577        // Leaf program - called twice
1578        let (_, leaf_program) = Program::<MainnetV0>::parse(
1579            r"
1580program leaf.aleo;
1581
1582mapping leaf_data:
1583    key as u64.public;
1584    value as u64.public;
1585
1586function leaf_fn:
1587    input r0 as u64.public;
1588    async leaf_fn r0 into r1;
1589    output r1 as leaf.aleo/leaf_fn.future;
1590
1591finalize leaf_fn:
1592    input r0 as u64.public;
1593    get.or_use leaf_data[r0] 0u64 into r1;
1594    add r1 1u64 into r2;
1595    set r2 into leaf_data[r0];
1596",
1597        )
1598        .unwrap();
1599
1600        // Level 1 program A - calls leaf
1601        let (_, level1_a_program) = Program::<MainnetV0>::parse(
1602            r"
1603import leaf.aleo;
1604
1605program level1_a.aleo;
1606
1607mapping level1_a_data:
1608    key as u64.public;
1609    value as u64.public;
1610
1611function fn_a:
1612    input r0 as u64.public;
1613    call leaf.aleo/leaf_fn r0 into r1;
1614    async fn_a r1 r0 into r2;
1615    output r2 as level1_a.aleo/fn_a.future;
1616
1617finalize fn_a:
1618    input r0 as leaf.aleo/leaf_fn.future;
1619    input r1 as u64.public;
1620    await r0;
1621    get.or_use level1_a_data[r1] 0u64 into r2;
1622    add r2 100u64 into r3;
1623    set r3 into level1_a_data[r1];
1624",
1625        )
1626        .unwrap();
1627
1628        // Level 1 program B - also calls leaf
1629        let (_, level1_b_program) = Program::<MainnetV0>::parse(
1630            r"
1631import leaf.aleo;
1632
1633program level1_b.aleo;
1634
1635mapping level1_b_data:
1636    key as u64.public;
1637    value as u64.public;
1638
1639function fn_b:
1640    input r0 as u64.public;
1641    call leaf.aleo/leaf_fn r0 into r1;
1642    async fn_b r1 r0 into r2;
1643    output r2 as level1_b.aleo/fn_b.future;
1644
1645finalize fn_b:
1646    input r0 as leaf.aleo/leaf_fn.future;
1647    input r1 as u64.public;
1648    await r0;
1649    get.or_use level1_b_data[r1] 0u64 into r2;
1650    add r2 200u64 into r3;
1651    set r3 into level1_b_data[r1];
1652",
1653        )
1654        .unwrap();
1655
1656        // Root program - calls both level1_a and level1_b
1657        // Note: must import leaf.aleo for transitive dependency resolution
1658        let (_, root_program) = Program::<MainnetV0>::parse(
1659            r"
1660import leaf.aleo;
1661import level1_a.aleo;
1662import level1_b.aleo;
1663
1664program root.aleo;
1665
1666mapping root_data:
1667    key as u64.public;
1668    value as u64.public;
1669
1670function main:
1671    input r0 as u64.public;
1672    call level1_a.aleo/fn_a r0 into r1;
1673    call level1_b.aleo/fn_b r0 into r2;
1674    async main r1 r2 r0 into r3;
1675    output r3 as root.aleo/main.future;
1676
1677finalize main:
1678    input r0 as level1_a.aleo/fn_a.future;
1679    input r1 as level1_b.aleo/fn_b.future;
1680    input r2 as u64.public;
1681    await r0;
1682    await r1;
1683    get.or_use root_data[r2] 0u64 into r3;
1684    add r3 1000u64 into r4;
1685    set r4 into root_data[r2];
1686",
1687        )
1688        .unwrap();
1689
1690        // Build the process with all programs
1691        let mut process = crate::test_helpers::sample_process(&leaf_program);
1692        process.add_program(&level1_a_program).unwrap();
1693        process.add_program(&level1_b_program).unwrap();
1694        process.add_program(&root_program).unwrap();
1695
1696        let function_name = Identifier::from_str("main").unwrap();
1697
1698        // Get execution
1699        let execution = get_execution(&mut process, &root_program, &function_name, ["42u64"].into_iter());
1700
1701        // Verify we have 5 transitions
1702        let num_transitions = execution.transitions().count();
1703        println!("Complex call graph - Number of transitions: {num_transitions}");
1704        assert_eq!(num_transitions, 5, "Expected 5 transitions for complex call graph");
1705
1706        // Get the stack for static cost calculation
1707        let stack = process.get_stack(root_program.id()).unwrap();
1708
1709        // Calculate costs using both methods
1710        let static_cost_v3 = minimum_cost_in_microcredits_v3(&stack, &function_name).unwrap();
1711        let runtime_cost_v3 = execution_finalize_cost(&process, &execution, ConsensusFeeVersion::V3).unwrap();
1712
1713        println!("Complex call graph - Static cost V3: {static_cost_v3}");
1714        println!("Complex call graph - Runtime cost V3: {runtime_cost_v3}");
1715
1716        assert_eq!(
1717            static_cost_v3, runtime_cost_v3,
1718            "V3: Static and runtime costs should match for complex call graph: static={static_cost_v3}, runtime={runtime_cost_v3}"
1719        );
1720
1721        // Also verify V1 and V2
1722        let static_cost_v1 = minimum_cost_in_microcredits_v1(&stack, &function_name).unwrap();
1723        let runtime_cost_v1 = execution_finalize_cost(&process, &execution, ConsensusFeeVersion::V1).unwrap();
1724        assert_eq!(static_cost_v1, runtime_cost_v1, "V1: Static and runtime costs should match for complex call graph");
1725
1726        let static_cost_v2 = minimum_cost_in_microcredits_v2(&stack, &function_name).unwrap();
1727        let runtime_cost_v2 = execution_finalize_cost(&process, &execution, ConsensusFeeVersion::V2).unwrap();
1728        assert_eq!(static_cost_v2, runtime_cost_v2, "V2: Static and runtime costs should match for complex call graph");
1729
1730        // Verify costs are meaningful
1731        assert!(static_cost_v3 > 0, "Expected non-zero cost");
1732        println!(
1733            "Complex call graph - V1 cost: {static_cost_v1}, V2 cost: {static_cost_v2}, V3 cost: {static_cost_v3}"
1734        );
1735    }
1736}