diff --git a/console/network/src/lib.rs b/console/network/src/lib.rs index 6598e42849..45b109c29c 100644 --- a/console/network/src/lib.rs +++ b/console/network/src/lib.rs @@ -104,6 +104,8 @@ pub trait Network: const MAX_DEPLOYMENT_LIMIT: u64 = 1 << 20; // 1,048,576 constraints /// The maximum number of microcredits that can be spent as a fee. const MAX_FEE: u64 = 1_000_000_000_000_000; + /// The maximum number of microcredits that can be spent on a finalize block. + const TRANSACTION_SPEND_LIMIT: u64 = 100_000_000; /// The anchor height, defined as the expected number of blocks to reach the coinbase target. const ANCHOR_HEIGHT: u32 = Self::ANCHOR_TIME as u32 / Self::BLOCK_TIME as u32; diff --git a/ledger/src/tests.rs b/ledger/src/tests.rs index 26744f15c4..8446e47e48 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::{program::Program, vm::VM}; +use synthesizer::{prelude::cost_in_microcredits, program::Program, vm::VM, Stack}; #[test] fn test_load() { @@ -1497,3 +1497,79 @@ fn test_max_committee_limit_with_bonds() { let committee = ledger.latest_committee().unwrap(); assert!(!committee.is_committee_member(second_address)); } + +#[test] +fn test_deployment_exceeding_max_transaction_spend() { + let rng = &mut TestRng::default(); + + // Initialize the test environment. + let crate::test_helpers::TestEnv { ledger, private_key, .. } = crate::test_helpers::sample_test_env(rng); + + // Construct two programs, one that is allowed and one that exceeds the maximum transaction spend. + let mut allowed_program = None; + let mut exceeding_program = None; + + for i in 0..::MAX_COMMANDS.ilog2() { + // Construct the finalize body. + let finalize_body = + (0..2.pow(i)).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_{i}.aleo; + function foo: + async foo into r0; + output r0 as test_max_spend_limit_{i}.aleo/foo.future; + + finalize foo:{finalize_body}", + )) + .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 { + exceeding_program = Some(program); + break; + } else { + allowed_program = Some(program); + } + } + + // Ensure that the allowed and exceeding programs are not None. + assert!(allowed_program.is_some()); + assert!(exceeding_program.is_some()); + + let allowed_program = allowed_program.unwrap(); + let exceeding_program = exceeding_program.unwrap(); + + // Deploy the allowed program. + let deployment = ledger.vm().deploy(&private_key, &allowed_program, None, 0, None, rng).unwrap(); + + // Verify the deployment transaction. + assert!(ledger.vm().check_transaction(&deployment, None, rng).is_ok()); + + // Construct the next block. + let block = + ledger.prepare_advance_to_next_beacon_block(&private_key, vec![], vec![], vec![deployment], rng).unwrap(); + + // Check that the next block is valid. + ledger.check_next_block(&block, rng).unwrap(); + + // Add the block to the ledger. + ledger.advance_to_next_block(&block).unwrap(); + + // 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(); + + // Verify the deployment transaction. + assert!(ledger.vm().check_transaction(&deployment, None, rng).is_err()); +} diff --git a/synthesizer/src/vm/helpers/cost.rs b/synthesizer/process/src/cost.rs similarity index 98% rename from synthesizer/src/vm/helpers/cost.rs rename to synthesizer/process/src/cost.rs index bcee10e70d..d30bc504f5 100644 --- a/synthesizer/src/vm/helpers/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -12,16 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{ - prelude::{Stack, StackProgramTypes}, - VM, -}; +use crate::{Process, Stack, StackProgramTypes}; + use console::{ prelude::*, program::{FinalizeType, Identifier, LiteralType, PlaintextType}, }; use ledger_block::{Deployment, Execution}; -use ledger_store::ConsensusStorage; use synthesizer_program::{CastType, Command, Finalize, Instruction, Operand, StackProgram}; /// Returns the *minimum* cost in microcredits to publish the given deployment (total cost, (storage cost, synthesis cost, namespace cost)). @@ -59,10 +56,7 @@ pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, ( } /// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)). -pub fn execution_cost>( - vm: &VM, - execution: &Execution, -) -> Result<(u64, (u64, u64))> { +pub fn execution_cost(process: &Process, execution: &Execution) -> Result<(u64, (u64, u64))> { // Compute the storage cost in microcredits. let storage_cost = execution.size_in_bytes()?; @@ -73,7 +67,7 @@ pub fn execution_cost>( // 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(vm.process().read().get_stack(program_id)?, function_name)?; + let cost = cost_in_microcredits(process.get_stack(program_id)?, function_name)?; // Accumulate the finalize cost. if cost > 0 { finalize_cost = finalize_cost diff --git a/synthesizer/process/src/lib.rs b/synthesizer/process/src/lib.rs index 53953ecad6..4beb82b95e 100644 --- a/synthesizer/process/src/lib.rs +++ b/synthesizer/process/src/lib.rs @@ -18,6 +18,9 @@ // TODO (howardwu): Update the return type on `execute` after stabilizing the interface. #![allow(clippy::type_complexity)] +mod cost; +pub use cost::*; + mod stack; pub use stack::*; diff --git a/synthesizer/process/src/verify_deployment.rs b/synthesizer/process/src/verify_deployment.rs index f499fdebcc..843bc5340b 100644 --- a/synthesizer/process/src/verify_deployment.rs +++ b/synthesizer/process/src/verify_deployment.rs @@ -33,6 +33,16 @@ 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/src/vm/execute.rs b/synthesizer/src/vm/execute.rs index b27626504d..2acd01d06f 100644 --- a/synthesizer/src/vm/execute.rs +++ b/synthesizer/src/vm/execute.rs @@ -45,7 +45,7 @@ impl> VM { let fee = match is_fee_required || is_priority_fee_declared { true => { // Compute the minimum execution cost. - let (minimum_execution_cost, (_, _)) = execution_cost(self, &execution)?; + let (minimum_execution_cost, (_, _)) = execution_cost(&self.process().read(), &execution)?; // Compute the execution ID. let execution_id = execution.to_execution_id()?; // Authorize the fee. diff --git a/synthesizer/src/vm/helpers/mod.rs b/synthesizer/src/vm/helpers/mod.rs index 2ef9403bee..1fd8ec7321 100644 --- a/synthesizer/src/vm/helpers/mod.rs +++ b/synthesizer/src/vm/helpers/mod.rs @@ -15,9 +15,6 @@ pub(crate) mod committee; pub use committee::*; -mod cost; -pub use cost::*; - mod macros; mod rewards; diff --git a/synthesizer/src/vm/mod.rs b/synthesizer/src/vm/mod.rs index ff71e846eb..795499689b 100644 --- a/synthesizer/src/vm/mod.rs +++ b/synthesizer/src/vm/mod.rs @@ -55,7 +55,7 @@ use ledger_store::{ TransactionStore, TransitionStore, }; -use synthesizer_process::{Authorization, Process, Trace}; +use synthesizer_process::{deployment_cost, execution_cost, Authorization, Process, Trace}; use synthesizer_program::{FinalizeGlobalState, FinalizeOperation, FinalizeStoreTrait, Program}; use aleo_std::prelude::{finish, lap, timer}; diff --git a/synthesizer/src/vm/verify.rs b/synthesizer/src/vm/verify.rs index 2a97b6e8d5..285565325f 100644 --- a/synthesizer/src/vm/verify.rs +++ b/synthesizer/src/vm/verify.rs @@ -183,7 +183,7 @@ impl> VM { // If the fee is required, then check that the base fee amount is satisfied. if is_fee_required { // Compute the execution cost. - let (cost, _) = execution_cost(self, execution)?; + let (cost, _) = execution_cost(&self.process().read(), execution)?; // Ensure the fee is sufficient to cover the cost. if *fee.base_amount()? < cost { bail!(