From 45fa678b814b9dee50de0ced647adc794411c091 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Mon, 4 Mar 2024 18:29:59 -0800 Subject: [PATCH 1/4] Refactor to expose command_cost --- synthesizer/process/src/cost.rs | 291 ++++++++++++++++---------------- 1 file changed, 145 insertions(+), 146 deletions(-) diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index ff9776db29..45cbe5e48e 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -21,150 +21,23 @@ use console::{ use ledger_block::{Deployment, Execution}; 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)). -pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, (u64, u64, u64))> { - // Determine the number of bytes in the deployment. - let size_in_bytes = deployment.size_in_bytes()?; - // Retrieve the program ID. - let program_id = deployment.program_id(); - // Determine the number of characters in the program ID. - let num_characters = u32::try_from(program_id.name().to_string().len())?; - // Compute the number of combined constraints in the program. - let num_combined_constraints = deployment.num_combined_constraints()?; - - // Compute the storage cost in microcredits. - let storage_cost = size_in_bytes - .checked_mul(N::DEPLOYMENT_FEE_MULTIPLIER) - .ok_or(anyhow!("The storage cost computation overflowed for a deployment"))?; - - // Compute the synthesis cost in microcredits. - let synthesis_cost = num_combined_constraints * N::SYNTHESIS_FEE_MULTIPLIER; - - // Compute the namespace cost in credits: 10^(10 - num_characters). - let namespace_cost = 10u64 - .checked_pow(10u32.saturating_sub(num_characters)) - .ok_or(anyhow!("The namespace cost computation overflowed for a deployment"))? - .saturating_mul(1_000_000); // 1 microcredit = 1e-6 credits. - - // Compute the total cost in microcredits. - let total_cost = storage_cost - .checked_add(synthesis_cost) - .and_then(|x| x.checked_add(namespace_cost)) - .ok_or(anyhow!("The total cost computation overflowed for a deployment"))?; - - Ok((total_cost, (storage_cost, synthesis_cost, namespace_cost))) -} - -/// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)). -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()?; - - // 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 - .checked_add(finalize_cost) - .ok_or(anyhow!("The total cost computation overflowed for an 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)`. - - const CAST_BASE_COST: u64 = 500; - const CAST_PER_BYTE_COST: u64 = 30; +/// The base and per byte costs for expensive operations. +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_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 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 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_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 SET_BASE_COST: u64 = 10_000; - const SET_PER_BYTE_COST: u64 = 100; - - // 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); - }; - - // 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 command_cost(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,6 +231,68 @@ pub fn cost_in_microcredits(stack: &Stack, function_name: &Identi } Command::BranchEq(_) | Command::BranchNeq(_) => Ok(500), Command::Position(_) => Ok(100), + } +} + +/// Returns the *minimum* cost in microcredits to publish the given deployment (total cost, (storage cost, synthesis cost, namespace cost)). +pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, (u64, u64, u64))> { + // Determine the number of bytes in the deployment. + let size_in_bytes = deployment.size_in_bytes()?; + // Retrieve the program ID. + let program_id = deployment.program_id(); + // Determine the number of characters in the program ID. + let num_characters = u32::try_from(program_id.name().to_string().len())?; + // Compute the number of combined constraints in the program. + let num_combined_constraints = deployment.num_combined_constraints()?; + + // Compute the storage cost in microcredits. + let storage_cost = size_in_bytes + .checked_mul(N::DEPLOYMENT_FEE_MULTIPLIER) + .ok_or(anyhow!("The storage cost computation overflowed for a deployment"))?; + + // Compute the synthesis cost in microcredits. + let synthesis_cost = num_combined_constraints * N::SYNTHESIS_FEE_MULTIPLIER; + + // Compute the namespace cost in credits: 10^(10 - num_characters). + let namespace_cost = 10u64 + .checked_pow(10u32.saturating_sub(num_characters)) + .ok_or(anyhow!("The namespace cost computation overflowed for a deployment"))? + .saturating_mul(1_000_000); // 1 microcredit = 1e-6 credits. + + // Compute the total cost in microcredits. + let total_cost = storage_cost + .checked_add(synthesis_cost) + .and_then(|x| x.checked_add(namespace_cost)) + .ok_or(anyhow!("The total cost computation overflowed for a deployment"))?; + + Ok((total_cost, (storage_cost, synthesis_cost, namespace_cost))) +} + +/// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)). +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()?; + + // 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 + .checked_add(finalize_cost) + .ok_or(anyhow!("The total cost computation overflowed for an 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 { + // 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. @@ -374,7 +309,71 @@ pub fn cost_in_microcredits(stack: &Stack, function_name: &Identi } // 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| command_cost(stack, finalize, command)) + .try_fold(future_cost, |acc, res| { + res.and_then(|x| acc.checked_add(x).ok_or(anyhow!("Finalize cost overflowed"))) + }) +} + +/// A helper function to determine the size of 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))) } From e5624870e50d8f3fc1d05568ac8e96a4304f1373 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Mon, 4 Mar 2024 18:50:48 -0800 Subject: [PATCH 2/4] Add test --- synthesizer/src/vm/execute.rs | 293 ++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) diff --git a/synthesizer/src/vm/execute.rs b/synthesizer/src/vm/execute.rs index 2acd01d06f..40f9ef35ed 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::command_cost; + 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| command_cost(&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| command_cost(&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); + } } From 5dd0660cb05ce9a7086052f42e44731032a10613 Mon Sep 17 00:00:00 2001 From: Pranav Gaddamadugu <23022326+d0cd@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:24:36 -0800 Subject: [PATCH 3/4] Reduce diff --- synthesizer/process/src/cost.rs | 240 ++++++++++++++++---------------- synthesizer/src/vm/execute.rs | 6 +- 2 files changed, 125 insertions(+), 121 deletions(-) diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index 45cbe5e48e..cdf1cdff13 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -21,22 +21,141 @@ use console::{ use ledger_block::{Deployment, Execution}; use synthesizer_program::{CastType, Command, Finalize, Instruction, Operand, StackProgram}; -/// The base and per byte costs for expensive operations. +/// Returns the *minimum* cost in microcredits to publish the given deployment (total cost, (storage cost, synthesis cost, namespace cost)). +pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, (u64, u64, u64))> { + // Determine the number of bytes in the deployment. + let size_in_bytes = deployment.size_in_bytes()?; + // Retrieve the program ID. + let program_id = deployment.program_id(); + // Determine the number of characters in the program ID. + let num_characters = u32::try_from(program_id.name().to_string().len())?; + // Compute the number of combined constraints in the program. + let num_combined_constraints = deployment.num_combined_constraints()?; + + // Compute the storage cost in microcredits. + let storage_cost = size_in_bytes + .checked_mul(N::DEPLOYMENT_FEE_MULTIPLIER) + .ok_or(anyhow!("The storage cost computation overflowed for a deployment"))?; + + // Compute the synthesis cost in microcredits. + let synthesis_cost = num_combined_constraints * N::SYNTHESIS_FEE_MULTIPLIER; + + // Compute the namespace cost in credits: 10^(10 - num_characters). + let namespace_cost = 10u64 + .checked_pow(10u32.saturating_sub(num_characters)) + .ok_or(anyhow!("The namespace cost computation overflowed for a deployment"))? + .saturating_mul(1_000_000); // 1 microcredit = 1e-6 credits. + + // Compute the total cost in microcredits. + let total_cost = storage_cost + .checked_add(synthesis_cost) + .and_then(|x| x.checked_add(namespace_cost)) + .ok_or(anyhow!("The total cost computation overflowed for a deployment"))?; + + Ok((total_cost, (storage_cost, synthesis_cost, namespace_cost))) +} + +/// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)). +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()?; + + // 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 + .checked_add(finalize_cost) + .ok_or(anyhow!("The total cost computation overflowed for an execution"))?; + + Ok((total_cost, (storage_cost, finalize_cost))) +} + +/// 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 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_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 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)) + } + } +} + +/// 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))) +} + /// Returns the the cost of a command in a finalize scope. -pub fn command_cost(stack: &Stack, finalize: &Finalize, command: &Command) -> Result { +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), @@ -234,59 +353,6 @@ pub fn command_cost(stack: &Stack, finalize: &Finalize, comman } } -/// Returns the *minimum* cost in microcredits to publish the given deployment (total cost, (storage cost, synthesis cost, namespace cost)). -pub fn deployment_cost(deployment: &Deployment) -> Result<(u64, (u64, u64, u64))> { - // Determine the number of bytes in the deployment. - let size_in_bytes = deployment.size_in_bytes()?; - // Retrieve the program ID. - let program_id = deployment.program_id(); - // Determine the number of characters in the program ID. - let num_characters = u32::try_from(program_id.name().to_string().len())?; - // Compute the number of combined constraints in the program. - let num_combined_constraints = deployment.num_combined_constraints()?; - - // Compute the storage cost in microcredits. - let storage_cost = size_in_bytes - .checked_mul(N::DEPLOYMENT_FEE_MULTIPLIER) - .ok_or(anyhow!("The storage cost computation overflowed for a deployment"))?; - - // Compute the synthesis cost in microcredits. - let synthesis_cost = num_combined_constraints * N::SYNTHESIS_FEE_MULTIPLIER; - - // Compute the namespace cost in credits: 10^(10 - num_characters). - let namespace_cost = 10u64 - .checked_pow(10u32.saturating_sub(num_characters)) - .ok_or(anyhow!("The namespace cost computation overflowed for a deployment"))? - .saturating_mul(1_000_000); // 1 microcredit = 1e-6 credits. - - // Compute the total cost in microcredits. - let total_cost = storage_cost - .checked_add(synthesis_cost) - .and_then(|x| x.checked_add(namespace_cost)) - .ok_or(anyhow!("The total cost computation overflowed for a deployment"))?; - - Ok((total_cost, (storage_cost, synthesis_cost, namespace_cost))) -} - -/// Returns the *minimum* cost in microcredits to publish the given execution (total cost, (storage cost, finalize cost)). -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()?; - - // 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 - .checked_add(finalize_cost) - .ok_or(anyhow!("The total cost computation overflowed for an 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 { // Retrieve the finalize logic. @@ -294,7 +360,6 @@ pub fn cost_in_microcredits(stack: &Stack, function_name: &Identi // 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() { @@ -307,73 +372,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(|command| command_cost(stack, finalize, command)) + .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"))) }) } - -/// A helper function to determine the size of 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))) -} diff --git a/synthesizer/src/vm/execute.rs b/synthesizer/src/vm/execute.rs index 40f9ef35ed..484d963d7c 100644 --- a/synthesizer/src/vm/execute.rs +++ b/synthesizer/src/vm/execute.rs @@ -214,7 +214,7 @@ mod tests { }; use ledger_block::Transition; use ledger_store::helpers::memory::ConsensusMemory; - use synthesizer_process::command_cost; + use synthesizer_process::cost_per_command; use synthesizer_program::StackProgram; use indexmap::IndexMap; @@ -559,7 +559,7 @@ finalize test: finalize_logic .commands() .iter() - .map(|command| command_cost(&stack, finalize_logic, command)) + .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"))) }) @@ -694,7 +694,7 @@ finalize test: finalize_logic .commands() .iter() - .map(|command| command_cost(&stack, finalize_logic, command)) + .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"))) }) From f066df81d0b76a06b5be973ff053c23bc8f7b9ff Mon Sep 17 00:00:00 2001 From: Howard Wu <9260812+howardwu@users.noreply.github.com> Date: Wed, 20 Mar 2024 09:05:06 -0600 Subject: [PATCH 4/4] Update synthesizer/process/src/cost.rs Signed-off-by: Howard Wu <9260812+howardwu@users.noreply.github.com> --- synthesizer/process/src/cost.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/synthesizer/process/src/cost.rs b/synthesizer/process/src/cost.rs index cdf1cdff13..f97a3bf45c 100644 --- a/synthesizer/process/src/cost.rs +++ b/synthesizer/process/src/cost.rs @@ -76,6 +76,7 @@ pub fn execution_cost(process: &Process, execution: &Execution /// 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;