diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index 8446e47e48..3ede876f82 100644 --- a/ledger/src/tests.rs +++ b/ledger/src/tests.rs @@ -28,7 +28,7 @@ use indexmap::IndexMap; use ledger_block::{ConfirmedTransaction, Rejected, Transaction}; use ledger_committee::{Committee, MIN_VALIDATOR_STAKE}; use ledger_store::{helpers::memory::ConsensusMemory, ConsensusStore}; -use synthesizer::{prelude::cost_in_microcredits, program::Program, vm::VM, Stack}; +use synthesizer::{program::Program, vm::VM, Stack}; #[test] fn test_load() { @@ -1525,15 +1525,9 @@ fn test_deployment_exceeding_max_transaction_spend() { )) .unwrap(); - // Initialize a stack for the program. - let stack = Stack::::new(&ledger.vm().process().read(), &program).unwrap(); - - // Check the finalize cost. - let finalize_cost = cost_in_microcredits(&stack, &Identifier::from_str("foo").unwrap()).unwrap(); - - // If the finalize cost exceeds the maximum transaction spend, assign the program to the exceeding program and break. - // Otherwise, assign the program to the allowed program and continue. - if finalize_cost > ::TRANSACTION_SPEND_LIMIT { + // Attempt to initialize a `Stack` for the program. + // If this fails, then by `Stack::initialize` the finalize cost exceeds the `TRANSACTION_SPEND_LIMIT`. + if Stack::::new(&ledger.vm().process().read(), &program).is_err() { exceeding_program = Some(program); break; } else { @@ -1567,9 +1561,9 @@ fn test_deployment_exceeding_max_transaction_spend() { // Check that the program exists in the VM. assert!(ledger.vm().contains_program(allowed_program.id())); - // Deploy the exceeding program. - let deployment = ledger.vm().deploy(&private_key, &exceeding_program, None, 0, None, rng).unwrap(); + // Attempt to deploy the exceeding program. + let result = ledger.vm().deploy(&private_key, &exceeding_program, None, 0, None, rng); - // Verify the deployment transaction. - assert!(ledger.vm().check_transaction(&deployment, None, rng).is_err()); + // Check that the deployment failed. + assert!(result.is_err()); } diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index d30bc504f5..ff9776db29 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -60,21 +60,11 @@ pub fn execution_cost(process: &Process, execution: &Execution // Compute the storage cost in microcredits. let storage_cost = execution.size_in_bytes()?; - // Compute the finalize cost in microcredits. - let mut finalize_cost = 0u64; - // Iterate over the transitions to accumulate the finalize cost. - for transition in execution.transitions() { - // Retrieve the program ID and function name. - let (program_id, function_name) = (transition.program_id(), transition.function_name()); - // Retrieve the finalize cost. - let cost = cost_in_microcredits(process.get_stack(program_id)?, function_name)?; - // Accumulate the finalize cost. - if cost > 0 { - finalize_cost = finalize_cost - .checked_add(cost) - .ok_or(anyhow!("The finalize cost computation overflowed on '{program_id}/{function_name}'"))?; - } - } + // Get the root transition. + let transition = execution.peek()?; + + // Get the finalize cost for the root transition. + let finalize_cost = process.get_stack(transition.program_id())?.get_finalize_cost(transition.function_name())?; // Compute the total cost in microcredits. let total_cost = storage_cost @@ -370,10 +360,21 @@ pub fn cost_in_microcredits(stack: &Stack, function_name: &Identi Command::Position(_) => Ok(100), }; + // Get the cost of finalizing all futures. + let mut future_cost = 0u64; + for input in finalize.inputs() { + if let FinalizeType::Future(future) = input.finalize_type() { + // Get the external stack for the future. + let stack = stack.get_external_stack(future.program_id())?; + // Accumulate the finalize cost of the future. + future_cost = future_cost + .checked_add(stack.get_finalize_cost(future.resource())?) + .ok_or(anyhow!("Finalize cost overflowed"))?; + } + } + // Aggregate the cost of all commands in the program. - finalize - .commands() - .iter() - .map(cost) - .try_fold(0u64, |acc, res| res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed")))) + finalize.commands().iter().map(cost).try_fold(future_cost, |acc, res| { + res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) + }) } diff --git a/synthesizer/process/src/stack/helpers/initialize.rs b/synthesizer/process/src/stack/helpers/initialize.rs index 98e008ba3b..ea1c35f370 100644 --- a/synthesizer/process/src/stack/helpers/initialize.rs +++ b/synthesizer/process/src/stack/helpers/initialize.rs @@ -28,6 +28,7 @@ impl Stack { proving_keys: Default::default(), verifying_keys: Default::default(), number_of_calls: Default::default(), + finalize_costs: Default::default(), program_depth: 0, }; @@ -82,6 +83,17 @@ impl Stack { ); // Add the number of calls to the stack. stack.number_of_calls.insert(*function.name(), num_calls); + + // Get the finalize cost. + let finalize_cost = cost_in_microcredits(&stack, function.name())?; + // Check that the finalize cost does not exceed the maximum. + ensure!( + finalize_cost <= N::TRANSACTION_SPEND_LIMIT, + "Finalize block '{}' has a cost '{finalize_cost}' which exceeds the transaction spend limit '{}'", + function.name(), + N::TRANSACTION_SPEND_LIMIT + ); + stack.finalize_costs.insert(*function.name(), finalize_cost); } // Return the stack. diff --git a/synthesizer/process/src/stack/mod.rs b/synthesizer/process/src/stack/mod.rs index 6e10f4946c..aa0f7af67b 100644 --- a/synthesizer/process/src/stack/mod.rs +++ b/synthesizer/process/src/stack/mod.rs @@ -36,7 +36,7 @@ mod evaluate; mod execute; mod helpers; -use crate::{traits::*, CallMetrics, Process, Trace}; +use crate::{cost_in_microcredits, traits::*, CallMetrics, Process, Trace}; use console::{ account::{Address, PrivateKey}, network::prelude::*, @@ -187,6 +187,8 @@ pub struct Stack { verifying_keys: Arc, VerifyingKey>>>, /// The mapping of function names to the number of calls. number_of_calls: IndexMap, usize>, + /// The mapping of function names to finalize cost. + finalize_costs: IndexMap, u64>, /// The program depth. program_depth: usize, } @@ -274,6 +276,15 @@ impl StackProgram for Stack { external_program.get_record(locator.resource()) } + /// Returns the expected finalize cost for the given function name. + #[inline] + fn get_finalize_cost(&self, function_name: &Identifier) -> Result { + self.finalize_costs + .get(function_name) + .copied() + .ok_or_else(|| anyhow!("Function '{function_name}' does not exist")) + } + /// Returns the function with the given function name. #[inline] fn get_function(&self, function_name: &Identifier) -> Result> { diff --git a/synthesizer/process/src/tests/test_execute.rs b/synthesizer/process/src/tests/test_execute.rs index 8ba8de479b..676e6d7a65 100644 --- a/synthesizer/process/src/tests/test_execute.rs +++ b/synthesizer/process/src/tests/test_execute.rs @@ -16,6 +16,7 @@ use crate::{ traits::{StackEvaluate, StackExecute}, CallStack, Process, + Stack, Trace, }; use circuit::{network::AleoV0, Aleo}; @@ -2611,3 +2612,32 @@ fn test_max_imports() { )); assert!(result.is_err()); } + +#[test] +fn test_program_exceeding_transaction_spend_limit() { + // Construct a finalize body whose finalize cost is excessively large. + let finalize_body = (0..::MAX_COMMANDS) + .map(|i| format!("hash.bhp256 0field into r{i} as field;")) + .collect::>() + .join("\n"); + // Construct the program. + let program = Program::from_str(&format!( + r"program test_max_spend_limit.aleo; + function foo: + async foo into r0; + output r0 as test_max_spend_limit.aleo/foo.future; + finalize foo:{finalize_body}", + )) + .unwrap(); + + // Initialize a `Process`. + let mut process = Process::::load().unwrap(); + + // Attempt to add the program to the process, which should fail. + let result = process.add_program(&program); + assert!(result.is_err()); + + // Attempt to initialize a `Stack` directly with the program, which should fail. + let result = Stack::initialize(&process, &program); + assert!(result.is_err()); +} diff --git a/synthesizer/process/src/verify_deployment.rs b/synthesizer/process/src/verify_deployment.rs index 843bc5340b..f499fdebcc 100644 --- a/synthesizer/process/src/verify_deployment.rs +++ b/synthesizer/process/src/verify_deployment.rs @@ -33,16 +33,6 @@ impl Process { let stack = Stack::new(self, deployment.program())?; lap!(timer, "Compute the stack"); - // Ensure that each finalize block does not exceed the `TRANSACTION_SPEND_LIMIT`. - for (function_name, _) in deployment.program().functions() { - let finalize_cost = cost_in_microcredits(&stack, function_name)?; - ensure!( - finalize_cost <= N::TRANSACTION_SPEND_LIMIT, - "Finalize block '{function_name}' has a cost '{finalize_cost}' which exceeds the transaction spend limit '{}'", - N::TRANSACTION_SPEND_LIMIT - ); - } - // Ensure the verifying keys are well-formed and the certificates are valid. let verification = stack.verify_deployment::(deployment, rng); lap!(timer, "Verify the deployment"); diff --git a/synthesizer/program/src/traits/stack_and_registers.rs b/synthesizer/program/src/traits/stack_and_registers.rs index 1b79996aeb..214ea1eee6 100644 --- a/synthesizer/program/src/traits/stack_and_registers.rs +++ b/synthesizer/program/src/traits/stack_and_registers.rs @@ -80,6 +80,9 @@ pub trait StackProgram { /// Returns `true` if the stack contains the external record. fn get_external_record(&self, locator: &Locator) -> Result<&RecordType>; + /// Returns the expected finalize cost for the given function name. + fn get_finalize_cost(&self, function_name: &Identifier) -> Result; + /// Returns the function with the given function name. fn get_function(&self, function_name: &Identifier) -> Result>;