Skip to content

Commit

Permalink
Merge pull request #2369 from AleoHQ/fix/finalize-cost-calculation
Browse files Browse the repository at this point in the history
[Fix/Optimize] Fixes and optimizes calculation of execution cost.
  • Loading branch information
howardwu authored Mar 3, 2024
2 parents 31684ee + 2375daa commit 46f2625
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 45 deletions.
22 changes: 8 additions & 14 deletions ledger/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -1525,15 +1525,9 @@ fn test_deployment_exceeding_max_transaction_spend() {
))
.unwrap();

// Initialize a stack for the program.
let stack = Stack::<CurrentNetwork>::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 > <CurrentNetwork as Network>::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::<CurrentNetwork>::new(&ledger.vm().process().read(), &program).is_err() {
exceeding_program = Some(program);
break;
} else {
Expand Down Expand Up @@ -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());
}
41 changes: 21 additions & 20 deletions synthesizer/process/src/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,11 @@ pub fn execution_cost<N: Network>(process: &Process<N>, execution: &Execution<N>
// 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
Expand Down Expand Up @@ -370,10 +360,21 @@ pub fn cost_in_microcredits<N: Network>(stack: &Stack<N>, 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")))
})
}
12 changes: 12 additions & 0 deletions synthesizer/process/src/stack/helpers/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ impl<N: Network> Stack<N> {
proving_keys: Default::default(),
verifying_keys: Default::default(),
number_of_calls: Default::default(),
finalize_costs: Default::default(),
program_depth: 0,
};

Expand Down Expand Up @@ -82,6 +83,17 @@ impl<N: Network> Stack<N> {
);
// 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.
Expand Down
13 changes: 12 additions & 1 deletion synthesizer/process/src/stack/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*,
Expand Down Expand Up @@ -187,6 +187,8 @@ pub struct Stack<N: Network> {
verifying_keys: Arc<RwLock<IndexMap<Identifier<N>, VerifyingKey<N>>>>,
/// The mapping of function names to the number of calls.
number_of_calls: IndexMap<Identifier<N>, usize>,
/// The mapping of function names to finalize cost.
finalize_costs: IndexMap<Identifier<N>, u64>,
/// The program depth.
program_depth: usize,
}
Expand Down Expand Up @@ -274,6 +276,15 @@ impl<N: Network> StackProgram<N> for Stack<N> {
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<N>) -> Result<u64> {
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<N>) -> Result<Function<N>> {
Expand Down
30 changes: 30 additions & 0 deletions synthesizer/process/src/tests/test_execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::{
traits::{StackEvaluate, StackExecute},
CallStack,
Process,
Stack,
Trace,
};
use circuit::{network::AleoV0, Aleo};
Expand Down Expand Up @@ -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..<CurrentNetwork as Network>::MAX_COMMANDS)
.map(|i| format!("hash.bhp256 0field into r{i} as field;"))
.collect::<Vec<_>>()
.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::<CurrentNetwork>::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());
}
10 changes: 0 additions & 10 deletions synthesizer/process/src/verify_deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,6 @@ impl<N: Network> Process<N> {
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::<A, R>(deployment, rng);
lap!(timer, "Verify the deployment");
Expand Down
3 changes: 3 additions & 0 deletions synthesizer/program/src/traits/stack_and_registers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ pub trait StackProgram<N: Network> {
/// Returns `true` if the stack contains the external record.
fn get_external_record(&self, locator: &Locator<N>) -> Result<&RecordType<N>>;

/// Returns the expected finalize cost for the given function name.
fn get_finalize_cost(&self, function_name: &Identifier<N>) -> Result<u64>;

/// Returns the function with the given function name.
fn get_function(&self, function_name: &Identifier<N>) -> Result<Function<N>>;

Expand Down

0 comments on commit 46f2625

Please sign in to comment.