diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index ff9776db29..f97a3bf45c 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -74,97 +74,90 @@ pub fn execution_cost(process: &Process, execution: &Execution Ok((total_cost, (storage_cost, finalize_cost))) } -/// Returns the minimum number of microcredits required to run the finalize. -pub fn cost_in_microcredits(stack: &Stack, function_name: &Identifier) -> Result { - /// A helper function to determine the plaintext type in bytes. - fn plaintext_size_in_bytes(stack: &Stack, plaintext_type: &PlaintextType) -> Result { - match plaintext_type { - PlaintextType::Literal(literal_type) => Ok(literal_type.size_in_bytes::() as u64), - PlaintextType::Struct(struct_name) => { - // Retrieve the struct from the stack. - let struct_ = stack.program().get_struct(struct_name)?; - // Retrieve the size of the struct name. - let size_of_name = struct_.name().to_bytes_le()?.len() as u64; - // Retrieve the size of all the members of the struct. - let size_of_members = struct_.members().iter().try_fold(0u64, |acc, (_, member_type)| { - acc.checked_add(plaintext_size_in_bytes(stack, member_type)?).ok_or(anyhow!( - "Overflowed while computing the size of the struct '{}/{struct_name}' - {member_type}", - stack.program_id() - )) - })?; - // Return the size of the struct. - Ok(size_of_name.saturating_add(size_of_members)) - } - PlaintextType::Array(array_type) => { - // Retrieve the number of elements in the array. - let num_elements = **array_type.length() as u64; - // Compute the size of an array element. - let size_of_element = plaintext_size_in_bytes(stack, array_type.next_element_type())?; - // Return the size of the array. - Ok(num_elements.saturating_mul(size_of_element)) - } - } - } - - /// A helper function to compute the following: base_cost + (byte_multiplier * size_of_operands). - fn cost_in_size<'a, N: Network>( - stack: &Stack, - finalize: &Finalize, - operands: impl IntoIterator>, - byte_multiplier: u64, - base_cost: u64, - ) -> Result { - // Retrieve the finalize types. - let finalize_types = stack.get_finalize_types(finalize.name())?; - // Compute the size of the operands. - let size_of_operands = operands.into_iter().try_fold(0u64, |acc, operand| { - // Determine the size of the operand. - let operand_size = match finalize_types.get_type_from_operand(stack, operand)? { - FinalizeType::Plaintext(plaintext_type) => plaintext_size_in_bytes(stack, &plaintext_type)?, - FinalizeType::Future(future) => { - bail!("Future '{future}' is not a valid operand in the finalize scope"); - } - }; - // Safely add the size to the accumulator. - acc.checked_add(operand_size).ok_or(anyhow!( - "Overflowed while computing the size of the operand '{operand}' in '{}/{}' (finalize)", - stack.program_id(), - finalize.name() - )) - })?; - // Return the cost. - Ok(base_cost.saturating_add(byte_multiplier.saturating_mul(size_of_operands))) - } +/// Finalize costs for compute heavy operations, derived as: +/// `BASE_COST + (PER_BYTE_COST * SIZE_IN_BYTES)`. - // Finalize costs for compute heavy operations, derived as: - // `BASE_COST + (PER_BYTE_COST * SIZE_IN_BYTES)`. +const CAST_BASE_COST: u64 = 500; +const CAST_PER_BYTE_COST: u64 = 30; - const CAST_BASE_COST: u64 = 500; - const CAST_PER_BYTE_COST: u64 = 30; +const HASH_BASE_COST: u64 = 10_000; +const HASH_PER_BYTE_COST: u64 = 30; - const HASH_BASE_COST: u64 = 10_000; - const HASH_PER_BYTE_COST: u64 = 30; +const HASH_BHP_BASE_COST: u64 = 50_000; +const HASH_BHP_PER_BYTE_COST: u64 = 300; - const HASH_BHP_BASE_COST: u64 = 50_000; - const HASH_BHP_PER_BYTE_COST: u64 = 300; +const HASH_PSD_BASE_COST: u64 = 40_000; +const HASH_PSD_PER_BYTE_COST: u64 = 75; - const HASH_PSD_BASE_COST: u64 = 40_000; - const HASH_PSD_PER_BYTE_COST: u64 = 75; +const MAPPING_BASE_COST: u64 = 10_000; +const MAPPING_PER_BYTE_COST: u64 = 10; - const MAPPING_BASE_COST: u64 = 10_000; - const MAPPING_PER_BYTE_COST: u64 = 10; +const SET_BASE_COST: u64 = 10_000; +const SET_PER_BYTE_COST: u64 = 100; - const SET_BASE_COST: u64 = 10_000; - const SET_PER_BYTE_COST: u64 = 100; +/// A helper function to determine the plaintext type in bytes. +fn plaintext_size_in_bytes(stack: &Stack, plaintext_type: &PlaintextType) -> Result { + match plaintext_type { + PlaintextType::Literal(literal_type) => Ok(literal_type.size_in_bytes::() as u64), + PlaintextType::Struct(struct_name) => { + // Retrieve the struct from the stack. + let struct_ = stack.program().get_struct(struct_name)?; + // Retrieve the size of the struct name. + let size_of_name = struct_.name().to_bytes_le()?.len() as u64; + // Retrieve the size of all the members of the struct. + let size_of_members = struct_.members().iter().try_fold(0u64, |acc, (_, member_type)| { + acc.checked_add(plaintext_size_in_bytes(stack, member_type)?).ok_or(anyhow!( + "Overflowed while computing the size of the struct '{}/{struct_name}' - {member_type}", + stack.program_id() + )) + })?; + // Return the size of the struct. + Ok(size_of_name.saturating_add(size_of_members)) + } + PlaintextType::Array(array_type) => { + // Retrieve the number of elements in the array. + let num_elements = **array_type.length() as u64; + // Compute the size of an array element. + let size_of_element = plaintext_size_in_bytes(stack, array_type.next_element_type())?; + // Return the size of the array. + Ok(num_elements.saturating_mul(size_of_element)) + } + } +} - // Retrieve the finalize logic. - let Some(finalize) = stack.get_function_ref(function_name)?.finalize_logic() else { - // Return a finalize cost of 0, if the function does not have a finalize scope. - return Ok(0); - }; +/// A helper function to compute the following: base_cost + (byte_multiplier * size_of_operands). +fn cost_in_size<'a, N: Network>( + stack: &Stack, + finalize: &Finalize, + operands: impl IntoIterator>, + byte_multiplier: u64, + base_cost: u64, +) -> Result { + // Retrieve the finalize types. + let finalize_types = stack.get_finalize_types(finalize.name())?; + // Compute the size of the operands. + let size_of_operands = operands.into_iter().try_fold(0u64, |acc, operand| { + // Determine the size of the operand. + let operand_size = match finalize_types.get_type_from_operand(stack, operand)? { + FinalizeType::Plaintext(plaintext_type) => plaintext_size_in_bytes(stack, &plaintext_type)?, + FinalizeType::Future(future) => { + bail!("Future '{future}' is not a valid operand in the finalize scope"); + } + }; + // Safely add the size to the accumulator. + acc.checked_add(operand_size).ok_or(anyhow!( + "Overflowed while computing the size of the operand '{operand}' in '{}/{}' (finalize)", + stack.program_id(), + finalize.name() + )) + })?; + // Return the cost. + Ok(base_cost.saturating_add(byte_multiplier.saturating_mul(size_of_operands))) +} - // Measure the cost of each command. - let cost = |command: &Command| match command { +/// Returns the the cost of a command in a finalize scope. +pub fn cost_per_command(stack: &Stack, finalize: &Finalize, command: &Command) -> Result { + match command { Command::Instruction(Instruction::Abs(_)) => Ok(500), Command::Instruction(Instruction::AbsWrapped(_)) => Ok(500), Command::Instruction(Instruction::Add(_)) => Ok(500), @@ -358,8 +351,16 @@ pub fn cost_in_microcredits(stack: &Stack, function_name: &Identi } Command::BranchEq(_) | Command::BranchNeq(_) => Ok(500), Command::Position(_) => Ok(100), - }; + } +} +/// Returns the minimum number of microcredits required to run the finalize. +pub fn cost_in_microcredits(stack: &Stack, function_name: &Identifier) -> Result { + // Retrieve the finalize logic. + let Some(finalize) = stack.get_function_ref(function_name)?.finalize_logic() else { + // Return a finalize cost of 0, if the function does not have a finalize scope. + return Ok(0); + }; // Get the cost of finalizing all futures. let mut future_cost = 0u64; for input in finalize.inputs() { @@ -372,9 +373,12 @@ pub fn cost_in_microcredits(stack: &Stack, function_name: &Identi .ok_or(anyhow!("Finalize cost overflowed"))?; } } - // Aggregate the cost of all commands in the program. - 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"))) - }) + finalize + .commands() + .iter() + .map(|command| cost_per_command(stack, finalize, command)) + .try_fold(future_cost, |acc, res| { + res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) + }) } diff --git a/synthesizer/src/vm/execute.rs b/synthesizer/src/vm/execute.rs index 2acd01d06f..484d963d7c 100644 --- a/synthesizer/src/vm/execute.rs +++ b/synthesizer/src/vm/execute.rs @@ -214,6 +214,8 @@ mod tests { }; use ledger_block::Transition; use ledger_store::helpers::memory::ConsensusMemory; + use synthesizer_process::cost_per_command; + use synthesizer_program::StackProgram; use indexmap::IndexMap; @@ -415,4 +417,295 @@ mod tests { let fee_size_in_bytes = fee.to_bytes_le().unwrap().len(); assert_eq!(1416, fee_size_in_bytes, "Update me if serialization has changed"); } + + #[test] + fn test_wide_nested_execution_cost() { + // Initialize an RNG. + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng); + + // Prepare the VM. + let (vm, _) = prepare_vm(rng).unwrap(); + + // Construct the child program. + let child_program = Program::from_str( + r" +program child.aleo; +mapping data: + key as field.public; + value as field.public; +function test: + input r0 as field.public; + input r1 as field.public; + async test r0 r1 into r2; + output r2 as child.aleo/test.future; +finalize test: + input r0 as field.public; + input r1 as field.public; + hash.bhp256 r0 into r2 as field; + hash.bhp256 r1 into r3 as field; + set r2 into data[r3];", + ) + .unwrap(); + + // Deploy the program. + let transaction = vm.deploy(&caller_private_key, &child_program, None, 0, None, rng).unwrap(); + + // Construct the next block. + let next_block = crate::test_helpers::sample_next_block(&vm, &caller_private_key, &[transaction], rng).unwrap(); + + // Add the next block to the VM. + vm.add_next_block(&next_block).unwrap(); + + // Construct the parent program. + let parent_program = Program::from_str( + r" +import child.aleo; +program parent.aleo; +function test: + call child.aleo/test 0field 1field into r0; + call child.aleo/test 2field 3field into r1; + call child.aleo/test 4field 5field into r2; + call child.aleo/test 6field 7field into r3; + call child.aleo/test 8field 9field into r4; + call child.aleo/test 10field 11field into r5; + call child.aleo/test 12field 13field into r6; + call child.aleo/test 14field 15field into r7; + call child.aleo/test 16field 17field into r8; + call child.aleo/test 18field 19field into r9; + call child.aleo/test 20field 21field into r10; + call child.aleo/test 22field 23field into r11; + call child.aleo/test 24field 25field into r12; + call child.aleo/test 26field 27field into r13; + call child.aleo/test 28field 29field into r14; + call child.aleo/test 30field 31field into r15; + async test r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 into r16; + output r16 as parent.aleo/test.future; +finalize test: + input r0 as child.aleo/test.future; + input r1 as child.aleo/test.future; + input r2 as child.aleo/test.future; + input r3 as child.aleo/test.future; + input r4 as child.aleo/test.future; + input r5 as child.aleo/test.future; + input r6 as child.aleo/test.future; + input r7 as child.aleo/test.future; + input r8 as child.aleo/test.future; + input r9 as child.aleo/test.future; + input r10 as child.aleo/test.future; + input r11 as child.aleo/test.future; + input r12 as child.aleo/test.future; + input r13 as child.aleo/test.future; + input r14 as child.aleo/test.future; + input r15 as child.aleo/test.future; + await r0; + await r1; + await r2; + await r3; + await r4; + await r5; + await r6; + await r7; + await r8; + await r9; + await r10; + await r11; + await r12; + await r13; + await r14; + await r15;", + ) + .unwrap(); + + // Deploy the program. + let transaction = vm.deploy(&caller_private_key, &parent_program, None, 0, None, rng).unwrap(); + + // Construct the next block. + let next_block = crate::test_helpers::sample_next_block(&vm, &caller_private_key, &[transaction], rng).unwrap(); + + // Add the next block to the VM. + vm.add_next_block(&next_block).unwrap(); + + // Execute the parent program. + let Transaction::Execute(_, execution, _) = vm + .execute(&caller_private_key, ("parent.aleo", "test"), Vec::>::new().iter(), None, 0, None, rng) + .unwrap() + else { + unreachable!("VM::execute always produces an `Execution`") + }; + + // Check that the number of transitions is correct. + // Change me if the `MAX_INPUTS` changes. + assert_eq!(execution.transitions().len(), ::MAX_INPUTS + 1); + + // Get the finalize cost of the execution. + let (_, (_, finalize_cost)) = execution_cost(&vm.process().read(), &execution).unwrap(); + + // Compute the expected cost as the sum of the cost in microcredits of each command in each finalize block of each transition in the execution. + let mut expected_cost = 0; + for transition in execution.transitions() { + // Get the program ID and name of the transition. + let program_id = transition.program_id(); + let function_name = transition.function_name(); + // Get the stack. + let stack = vm.process().read().get_stack(program_id).unwrap().clone(); + // Get the finalize block of the transition and sum the cost of each command. + let cost = match stack.get_function(function_name).unwrap().finalize_logic() { + None => 0, + Some(finalize_logic) => { + // Aggregate the cost of all commands in the program. + finalize_logic + .commands() + .iter() + .map(|command| cost_per_command(&stack, finalize_logic, command)) + .try_fold(0u64, |acc, res| { + res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) + }) + .unwrap() + } + }; + // Add the cost to the total cost. + expected_cost += cost; + } + + // Check that the finalize cost is equal to the expected cost. + assert_eq!(finalize_cost, expected_cost); + } + + #[test] + fn test_deep_nested_execution_cost() { + // Initialize an RNG. + let rng = &mut TestRng::default(); + + // Initialize a new caller. + let caller_private_key = crate::vm::test_helpers::sample_genesis_private_key(rng); + + // Prepare the VM. + let (vm, _) = prepare_vm(rng).unwrap(); + + // Construct the base program. + let base_program = Program::from_str( + r" +program test_1.aleo; +mapping data: + key as field.public; + value as field.public; +function test: + input r0 as field.public; + input r1 as field.public; + async test r0 r1 into r2; + output r2 as test_1.aleo/test.future; +finalize test: + input r0 as field.public; + input r1 as field.public; + hash.bhp256 r0 into r2 as field; + hash.bhp256 r1 into r3 as field; + set r2 into data[r3];", + ) + .unwrap(); + + // Deploy the program. + let transaction = vm.deploy(&caller_private_key, &base_program, None, 0, None, rng).unwrap(); + + // Construct the next block. + let next_block = crate::test_helpers::sample_next_block(&vm, &caller_private_key, &[transaction], rng).unwrap(); + + // Add the next block to the VM. + vm.add_next_block(&next_block).unwrap(); + + // Initialize programs up to the maximum depth. + for i in 2..=Transaction::::MAX_TRANSITIONS - 1 { + // Construct the program. + let program = Program::from_str(&format!( + r" +{imports} +program test_{curr}.aleo; +mapping data: + key as field.public; + value as field.public; +function test: + input r0 as field.public; + input r1 as field.public; + call test_{prev}.aleo/test r0 r1 into r2; + async test r0 r1 r2 into r3; + output r3 as test_{curr}.aleo/test.future; +finalize test: + input r0 as field.public; + input r1 as field.public; + input r2 as test_{prev}.aleo/test.future; + await r2; + hash.bhp256 r0 into r3 as field; + hash.bhp256 r1 into r4 as field; + set r3 into data[r4];", + imports = (1..i).map(|j| format!("import test_{j}.aleo;")).join("\n"), + prev = i - 1, + curr = i, + )) + .unwrap(); + + // Deploy the program. + let transaction = vm.deploy(&caller_private_key, &program, None, 0, None, rng).unwrap(); + + // Construct the next block. + let next_block = + crate::test_helpers::sample_next_block(&vm, &caller_private_key, &[transaction], rng).unwrap(); + + // Add the next block to the VM. + vm.add_next_block(&next_block).unwrap(); + } + + // Execute the program. + let Transaction::Execute(_, execution, _) = vm + .execute( + &caller_private_key, + (format!("test_{}.aleo", Transaction::::MAX_TRANSITIONS - 1), "test"), + vec![Value::from_str("0field").unwrap(), Value::from_str("1field").unwrap()].iter(), + None, + 0, + None, + rng, + ) + .unwrap() + else { + unreachable!("VM::execute always produces an `Execution`") + }; + + // Check that the number of transitions is correct. + assert_eq!(execution.transitions().len(), Transaction::::MAX_TRANSITIONS - 1); + + // Get the finalize cost of the execution. + let (_, (_, finalize_cost)) = execution_cost(&vm.process().read(), &execution).unwrap(); + + // Compute the expected cost as the sum of the cost in microcredits of each command in each finalize block of each transition in the execution. + let mut expected_cost = 0; + for transition in execution.transitions() { + // Get the program ID and name of the transition. + let program_id = transition.program_id(); + let function_name = transition.function_name(); + // Get the stack. + let stack = vm.process().read().get_stack(program_id).unwrap().clone(); + // Get the finalize block of the transition and sum the cost of each command. + let cost = match stack.get_function(function_name).unwrap().finalize_logic() { + None => 0, + Some(finalize_logic) => { + // Aggregate the cost of all commands in the program. + finalize_logic + .commands() + .iter() + .map(|command| cost_per_command(&stack, finalize_logic, command)) + .try_fold(0u64, |acc, res| { + res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) + }) + .unwrap() + } + }; + // Add the cost to the total cost. + expected_cost += cost; + } + + // Check that the finalize cost is equal to the expected cost. + assert_eq!(finalize_cost, expected_cost); + } }