Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fix/Optimize] Fixes and optimizes calculation of execution cost. #2369

Merged
merged 6 commits into from Mar 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 8 additions & 14 deletions ledger/src/tests.rs
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
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
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
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
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
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
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