Skip to content

Commit

Permalink
Merge pull request #2356 from AleoHQ/fix/future-checks
Browse files Browse the repository at this point in the history
[Fix] Relax ordering checks for futures
  • Loading branch information
howardwu committed Mar 3, 2024
2 parents d487c56 + f8ca467 commit 31684ee
Show file tree
Hide file tree
Showing 19 changed files with 477 additions and 232 deletions.
55 changes: 33 additions & 22 deletions synthesizer/process/src/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
// limitations under the License.

use super::*;
use console::program::{Future, Register};
use console::program::{FinalizeType, Future, Register};
use synthesizer_program::{Await, FinalizeRegistersState, Operand};
use utilities::handle_halting;

use std::collections::HashSet;

impl<N: Network> Process<N> {
/// Finalizes the deployment and fee.
/// This method assumes the given deployment **is valid**.
Expand Down Expand Up @@ -209,13 +211,13 @@ fn finalize_transition<N: Network, P: FinalizeStorage<N>>(
states.push(initialize_finalize_state(state, future, stack, *transition.id())?);

// While there are active finalize states, finalize them.
while let Some(FinalizeState {
'outer: while let Some(FinalizeState {
mut counter,
finalize,
mut registers,
stack,
mut call_counter,
mut recent_call_locator,
mut awaited,
}) = states.pop()
{
// Evaluate the commands.
Expand Down Expand Up @@ -253,18 +255,16 @@ fn finalize_transition<N: Network, P: FinalizeStorage<N>>(
}
}
Command::Await(await_) => {
// Check that the `await` register's locator is greater than the last seen call locator.
// This ensures that futures are invoked in the order they are called.
let locator = *match await_.register() {
Register::Locator(locator) => locator,
Register::Access(..) => bail!("The 'await' register must be a locator"),
// Check that the `await` register's is a locator.
if let Register::Access(_, _) = await_.register() {
bail!("The 'await' register must be a locator")
};
if let Some(recent_call_locator) = recent_call_locator {
ensure!(
locator > recent_call_locator,
"Await register's locator '{locator}' must be greater than the last seen call locator '{recent_call_locator}'",
)
}
// Check that the future has not previously been awaited.
ensure!(
!awaited.contains(await_.register()),
"The future register '{}' has already been awaited",
await_.register()
);

// Get the current transition ID.
let transition_id = registers.transition_id();
Expand All @@ -288,23 +288,22 @@ fn finalize_transition<N: Network, P: FinalizeStorage<N>>(
Err(_) => bail!("'finalize' failed to evaluate command ({command})"),
};

// Set the last seen call locator.
recent_call_locator = Some(locator);
// Increment the call counter.
call_counter += 1;
// Increment the counter.
counter += 1;
// Add the awaited register to the tracked set.
awaited.insert(await_.register().clone());

// Aggregate the caller state.
let caller_state =
FinalizeState { counter, finalize, registers, stack, call_counter, recent_call_locator };
let caller_state = FinalizeState { counter, finalize, registers, stack, call_counter, awaited };

// Push the caller state onto the stack.
states.push(caller_state);
// Push the callee state onto the stack.
states.push(callee_state);

break;
continue 'outer;
}
_ => {
let result =
Expand All @@ -323,6 +322,18 @@ fn finalize_transition<N: Network, P: FinalizeStorage<N>>(
}
};
}
// Check that all future registers have been awaited.
let mut unawaited = Vec::new();
for input in finalize.inputs() {
if matches!(input.finalize_type(), FinalizeType::Future(_)) && !awaited.contains(input.register()) {
unawaited.push(input.register().clone());
}
}
ensure!(
unawaited.is_empty(),
"The following future registers have not been awaited: {}",
unawaited.iter().map(|r| r.to_string()).collect::<Vec<_>>().join(", ")
);
}

// Return the finalize operations.
Expand All @@ -341,8 +352,8 @@ struct FinalizeState<'a, N: Network> {
stack: &'a Stack<N>,
// Call counter.
call_counter: usize,
// Recent call register.
recent_call_locator: Option<u64>,
// Awaited futures.
awaited: HashSet<Register<N>>,
}

// A helper function to initialize the finalize state.
Expand Down Expand Up @@ -385,7 +396,7 @@ fn initialize_finalize_state<'a, N: Network>(
},
)?;

Ok(FinalizeState { counter: 0, finalize, registers, stack, call_counter: 0, recent_call_locator: None })
Ok(FinalizeState { counter: 0, finalize, registers, stack, call_counter: 0, awaited: Default::default() })
}

// A helper function that sets up the await operation.
Expand Down
44 changes: 19 additions & 25 deletions synthesizer/process/src/stack/finalize_types/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,16 @@
// limitations under the License.

use super::*;
use crate::RegisterTypes;
use synthesizer_program::{
Await,
Branch,
CallOperator,
CastType,
Contains,
Get,
GetOrUse,
RandChaCha,
Remove,
Set,
MAX_ADDITIONAL_SEEDS,
};

impl<N: Network> FinalizeTypes<N> {
/// Initializes a new instance of `FinalizeTypes` for the given finalize.
/// Checks that the given finalize is well-formed for the given stack.
///
/// Attention: To support user-defined ordering for awaiting on futures, this method does **not** check
/// that all input futures are awaited **exactly** once. It does however check that all input
/// futures are awaited at least once. This means that it is possible to deploy a program
/// whose finalize is not well-formed, but it is not possible to execute a program whose finalize
/// is not well-formed.
#[inline]
pub(super) fn initialize_finalize_types(
stack: &(impl StackMatches<N> + StackProgram<N>),
Expand All @@ -53,31 +45,33 @@ impl<N: Network> FinalizeTypes<N> {
}
}

// Initialize a list of consumed futures.
let mut consumed_futures = Vec::new();
// Initialize the set of consumed futures.
let mut consumed_futures = HashSet::new();

// Step 2. Check the commands are well-formed. Store the futures consumed by the `await` commands.
// Step 2. Check the commands are well-formed. Make sure all the input futures are awaited.
for command in finalize.commands() {
// Check the command opcode, operands, and destinations.
finalize_types.check_command(stack, finalize, command)?;

// If the command is an `await`, add the future to the list of consumed futures.
// If the command is an `await`, add the future to the set of consumed futures.
if let Command::Await(await_) = command {
// Note: `check_command` ensures that the register is a future. This is an additional check.
let locator = match finalize_types.get_type(stack, await_.register())? {
FinalizeType::Future(locator) => locator,
FinalizeType::Plaintext(..) => bail!("Expected a future in '{await_}'"),
};
consumed_futures.push((await_.register(), locator));
consumed_futures.insert((await_.register(), locator));
}
}

// Check that the input futures are consumed in the order they are passed in.
ensure!(
input_futures == consumed_futures,
"Futures in finalize '{}' are not awaited in the order they are passed in.",
finalize.name()
);
// Check that all input futures are consumed.
for input_future in &input_futures {
ensure!(
consumed_futures.contains(input_future),
"Futures in finalize '{}' are not all awaited.",
finalize.name()
)
}

Ok(finalize_types)
}
Expand Down
28 changes: 26 additions & 2 deletions synthesizer/process/src/stack/finalize_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,48 @@
mod initialize;
mod matches;

use crate::RegisterTypes;

use console::{
network::prelude::*,
program::{ArrayType, Identifier, LiteralType, PlaintextType, Register, RegisterType, StructType},
program::{
Access,
ArrayType,
FinalizeType,
Identifier,
LiteralType,
Locator,
PlaintextType,
Register,
RegisterType,
StructType,
},
};
use synthesizer_program::{
Await,
Branch,
CallOperator,
CastType,
Command,
Contains,
Finalize,
Get,
GetOrUse,
Instruction,
InstructionTrait,
Opcode,
Operand,
Program,
RandChaCha,
Remove,
Set,
StackMatches,
StackProgram,
MAX_ADDITIONAL_SEEDS,
};

use console::program::{Access, FinalizeType, Locator};
use indexmap::IndexMap;
use std::collections::HashSet;

#[derive(Clone, Default, PartialEq, Eq)]
pub struct FinalizeTypes<N: Network> {
Expand Down
37 changes: 16 additions & 21 deletions synthesizer/process/src/stack/register_types/initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
// limitations under the License.

use super::*;
use synthesizer_program::CastType;

impl<N: Network> RegisterTypes<N> {
/// Initializes a new instance of `RegisterTypes` for the given closure.
Expand Down Expand Up @@ -158,17 +157,17 @@ impl<N: Network> RegisterTypes<N> {
}

/* Additional checks. */
// - All futures produces before the `async` call must be consumed by the `async` call, in the order in which they were produced.
// - All futures produces before the `async` call must be consumed by the `async` call.

// Get all registers containing futures.
let mut future_registers = register_types
let mut future_registers: IndexSet<(Register<N>, Locator<N>)> = register_types
.destinations
.iter()
.filter_map(|(index, register_type)| match register_type {
RegisterType::Future(locator) => Some((Register::Locator(*index), *locator)),
RegisterType::Future(locator) => Some((Register::<N>::Locator(*index), *locator)),
_ => None,
})
.collect::<Vec<_>>();
.collect();

match async_ {
// If no `async` instruction exists, then there should not be any future registers.
Expand All @@ -179,26 +178,22 @@ impl<N: Network> RegisterTypes<N> {
function.name()
)
}
// Otherwise, check that all the registers were consumed by the `async` call, in order.
// Otherwise, check that all the registers were consumed by the `async` call.
Some(async_) => {
// Remove the last future, since this is the future created by the `async` call.
future_registers.pop();
// Get the register operands that are `future` types.
let async_future_operands = async_
.operands()
.iter()
.filter_map(|operand| match operand {
Operand::Register(register) => match register_types.get_type(stack, register).ok() {
Some(RegisterType::Future(locator)) => Some((register.clone(), locator)),
_ => None,
},
_ => None,
})
.collect::<Vec<_>>();
// Ensure the future operands are in the same order as the future registers.
// Check only the register operands that are `future` types.
for operand in async_.operands() {
if let Operand::Register(register) = operand {
if let Ok(RegisterType::Future(locator)) = register_types.get_type(stack, register) {
assert!(future_registers.remove(&(register.clone(), locator)));
}
}
}
// Ensure that all the futures created are consumed in the async call.
ensure!(
async_future_operands == future_registers,
"Function '{}' contains futures, but the 'async' instruction does not consume all of them in the order they were produced",
future_registers.is_empty(),
"Function '{}' contains futures, but the 'async' instruction does not consume all of the ones produced.",
function.name()
);
}
Expand Down
3 changes: 2 additions & 1 deletion synthesizer/process/src/stack/register_types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ use console::{
};
use synthesizer_program::{
CallOperator,
CastType,
Closure,
Function,
Instruction,
Expand All @@ -45,7 +46,7 @@ use synthesizer_program::{
};

use console::program::{FinalizeType, Locator};
use indexmap::IndexMap;
use indexmap::{IndexMap, IndexSet};

#[derive(Clone, Default, PartialEq, Eq)]
pub struct RegisterTypes<N: Network> {
Expand Down

0 comments on commit 31684ee

Please sign in to comment.