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] Relax ordering checks for futures #2356

Merged
merged 9 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
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!(
d0cd marked this conversation as resolved.
Show resolved Hide resolved
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