Skip to content

Commit

Permalink
[1/x][dev-inspect] Adds execution mode for a more expressive dry-run
Browse files Browse the repository at this point in the history
- Introduces dev-inspect transaction type that can call any Move function
- Allows return values from that Move function
- Also provides much more detailed error message information
  • Loading branch information
tnowacki committed Dec 7, 2022
1 parent 4b4fb4c commit 340832a
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 226 deletions.
104 changes: 55 additions & 49 deletions crates/sui-adapter/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,7 @@ use sui_verifier::{
};
use tracing::instrument;

macro_rules! assert_invariant {
($cond:expr, $msg:expr) => {
if !$cond {
return Err(ExecutionError::new_with_source(
ExecutionErrorKind::InvariantViolation,
$msg,
));
}
};
}
use crate::execution_mode::{self, ExecutionMode};

pub fn new_move_vm(natives: NativeFunctionTable) -> Result<MoveVM, SuiError> {
MoveVM::new_with_config(
Expand Down Expand Up @@ -111,6 +102,7 @@ pub fn new_session<
#[allow(clippy::too_many_arguments)]
#[instrument(name = "adapter_execute", level = "trace", skip_all)]
pub fn execute<
Mode: ExecutionMode,
E: Debug,
S: ResourceResolver<Error = E>
+ ModuleResolver<Error = E>
Expand All @@ -126,33 +118,35 @@ pub fn execute<
args: Vec<CallArg>,
gas_status: &mut GasStatus,
ctx: &mut TxContext,
) -> Result<(), ExecutionError> {
let objects = args
.iter()
.filter_map(|arg| match arg {
CallArg::Pure(_) => None,
) -> Result<Mode::ExecutionResult, ExecutionError> {
let mut objects: BTreeMap<ObjectID, &Object> = BTreeMap::new();
for arg in &args {
match arg {
CallArg::Pure(_) => continue,
CallArg::Object(ObjectArg::ImmOrOwnedObject((id, _, _)))
| CallArg::Object(ObjectArg::SharedObject { id, .. }) => {
Some(vec![(*id, state_view.read_object(id)?)])
let obj = state_view.read_object(id);
assert_invariant!(obj.is_some(), format!("Object {} does not exist yet", id));
objects.insert(*id, obj.unwrap());
}
CallArg::ObjVec(vec) => {
if vec.is_empty() {
return None;
CallArg::ObjVec(obj_args) => {
for obj_arg in obj_args {
match obj_arg {
ObjectArg::ImmOrOwnedObject((id, _, _))
| ObjectArg::SharedObject { id, .. } => {
let obj = state_view.read_object(id);
assert_invariant!(
obj.is_some(),
format!("Object {} does not exist yet", id)
);
objects.insert(*id, obj.unwrap());
}
}
}
Some(
vec.iter()
.filter_map(|obj_arg| match obj_arg {
ObjectArg::ImmOrOwnedObject((id, _, _))
| ObjectArg::SharedObject { id, .. } => {
Some((*id, state_view.read_object(id)?))
}
})
.collect(),
)
}
})
.flatten()
.collect();
}
}

let module = vm.load_module(&module_id, state_view)?;
let is_genesis = ctx.digest() == TransactionDigest::genesis();
let TypeCheckSuccess {
Expand All @@ -162,12 +156,20 @@ pub fn execute<
by_value_objects,
mutable_ref_objects,
has_ctx_arg,
} = resolve_and_type_check(&objects, &module, function, &type_args, args, is_genesis)?;
} = resolve_and_type_check(
&objects,
&module,
function,
&type_args,
args,
is_genesis,
Mode::allow_arbitrary_function_calls(),
)?;

if has_ctx_arg {
args.push(ctx.to_vec());
}
execute_internal(
execute_internal::<Mode, _, _>(
vm,
state_view,
&module_id,
Expand All @@ -187,6 +189,7 @@ pub fn execute<
/// call.
#[allow(clippy::too_many_arguments)]
fn execute_internal<
Mode: ExecutionMode,
E: Debug,
S: ResourceResolver<Error = E>
+ ModuleResolver<Error = E>
Expand All @@ -206,7 +209,7 @@ fn execute_internal<
mut mutable_ref_objects: BTreeMap<LocalIndex, ObjectID>,
gas_status: &mut GasStatus, // gas status for the current call operation
ctx: &mut TxContext,
) -> Result<(), ExecutionError> {
) -> Result<Mode::ExecutionResult, ExecutionError> {
let input_objects = object_data
.iter()
.map(|(id, (owner, _))| (*id, (by_value_objects.contains(id), *owner)))
Expand All @@ -219,16 +222,14 @@ fn execute_internal<
.map_err(|e| convert_type_argument_error(idx, e))?;
}
// script visibility checked manually for entry points
let (
SerializedReturnValues {
mut mutable_reference_outputs,
return_values,
},
(change_set, events, mut native_context_extensions),
) = session
let (result, (change_set, events, mut native_context_extensions)) = session
.execute_function_bypass_visibility(module_id, function, type_args, args, gas_status)
.and_then(|ret| Ok((ret, session.finish_with_extensions()?)))?;
assert_invariant!(return_values.is_empty(), "Return values must be empty");
let mode_result = Mode::make_result(&result)?;
let SerializedReturnValues {
mut mutable_reference_outputs,
..
} = result;
let object_runtime: ObjectRuntime = native_context_extensions.remove();
std::mem::drop(native_context_extensions);

Expand Down Expand Up @@ -314,8 +315,7 @@ fn execute_internal<
user_events,
ctx,
)?;

Ok(())
Ok(mode_result)
}

#[instrument(name = "adapter_publish", level = "trace", skip_all)]
Expand Down Expand Up @@ -428,8 +428,7 @@ fn init_modules<
}
args.push(ctx.to_vec());
let has_ctx_arg = true;

execute_internal(
execute_internal::<execution_mode::Normal, _, _>(
vm,
state_view,
&module_id,
Expand Down Expand Up @@ -695,6 +694,7 @@ pub fn resolve_and_type_check(
type_args: &[TypeTag],
args: Vec<CallArg>,
is_genesis: bool,
allow_arbitrary_function_calls: bool,
) -> Result<TypeCheckSuccess, ExecutionError> {
// Resolve the function we are calling
let view = &BinaryIndexedView::Module(module);
Expand All @@ -715,12 +715,15 @@ pub fn resolve_and_type_check(
));
}
};
// Check for entry modifier, but ignore for genesis.
// Check for entry modifier, but ignore for genesis or dev-inspect.
// Genesis calls non-entry, private functions, and bypasses this rule. This is helpful for
// ensuring the functions are not called again later.
// In other words, this is an implementation detail that we are using `execute` for genesis
// functions, and as such need to bypass this check.
if !fdef.is_entry && !is_genesis {
// Similarly, we will bypass this check for dev-inspect, as the mode does not make state changes
// and is just for developers to check the result of Move functions. This mode is flagged by
// allow_arbitrary_function_calls
if !fdef.is_entry && !is_genesis && !allow_arbitrary_function_calls {
return Err(ExecutionError::new_with_source(
ExecutionErrorKind::NonEntryFunctionInvoked,
"Can only call `entry` functions",
Expand Down Expand Up @@ -779,6 +782,9 @@ pub fn resolve_and_type_check(
let param_type = &parameters[idx];
let idx = idx as LocalIndex;
let object_arg = match arg {
// dev-inspect does not make state changes and just a developer aid, so let through
// any BCS bytes (they will be checked later by the VM)
CallArg::Pure(arg) if allow_arbitrary_function_calls => return Ok(arg),
CallArg::Pure(arg) => {
let (is_primitive, type_layout_opt) =
primitive_type(view, type_args, param_type);
Expand Down
84 changes: 84 additions & 0 deletions crates/sui-adapter/src/execution_mode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use move_vm_runtime::session::SerializedReturnValues;
use sui_types::error::ExecutionError;

pub type TransactionIndex = usize;

pub trait ExecutionMode {
type ExecutionResult;
type ExecutionResults;

fn allow_arbitrary_function_calls() -> bool;

fn make_result(
return_values: &SerializedReturnValues,
) -> Result<Self::ExecutionResult, ExecutionError>;

fn empty_results() -> Self::ExecutionResults;

fn add_result(
results: &mut Self::ExecutionResults,
idx: TransactionIndex,
result: Self::ExecutionResult,
);
}

pub struct Normal;

impl ExecutionMode for Normal {
type ExecutionResult = ();
type ExecutionResults = ();

fn allow_arbitrary_function_calls() -> bool {
false
}

fn make_result(srv: &SerializedReturnValues) -> Result<Self::ExecutionResult, ExecutionError> {
assert_invariant!(srv.return_values.is_empty(), "Return values must be empty");
Ok(())
}

fn empty_results() -> Self::ExecutionResults {
()
}

fn add_result(_: &mut Self::ExecutionResults, _: TransactionIndex, _: Self::ExecutionResult) {
()
}
}

pub struct DevInspect;

impl ExecutionMode for DevInspect {
type ExecutionResult = SerializedReturnValues;
type ExecutionResults = Vec<(TransactionIndex, SerializedReturnValues)>;

fn allow_arbitrary_function_calls() -> bool {
true
}

fn make_result(srv: &SerializedReturnValues) -> Result<Self::ExecutionResult, ExecutionError> {
let SerializedReturnValues {
mutable_reference_outputs,
return_values,
} = srv;
Ok(SerializedReturnValues {
mutable_reference_outputs: mutable_reference_outputs.clone(),
return_values: return_values.clone(),
})
}

fn empty_results() -> Self::ExecutionResults {
todo!()
}

fn add_result(
results: &mut Self::ExecutionResults,
idx: TransactionIndex,
result: Self::ExecutionResult,
) {
results.push((idx, result))
}
}
12 changes: 12 additions & 0 deletions crates/sui-adapter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

macro_rules! assert_invariant {
($cond:expr, $msg:expr) => {
if !$cond {
return Err(sui_types::error::ExecutionError::new_with_source(
sui_types::error::ExecutionErrorKind::InvariantViolation,
$msg,
));
}
};
}

pub mod adapter;
pub mod execution_mode;
pub mod genesis;
4 changes: 2 additions & 2 deletions crates/sui-config/src/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use serde_with::serde_as;
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::{fs, path::Path};
use sui_adapter::adapter;
use sui_adapter::adapter::MoveVM;
use sui_adapter::{adapter, execution_mode};
use sui_types::base_types::ObjectID;
use sui_types::base_types::TransactionDigest;
use sui_types::crypto::{AuthorityPublicKey, ToFromBytes};
Expand Down Expand Up @@ -565,7 +565,7 @@ pub fn generate_genesis_system_object(
commission_rates.push(validator.commission_rate());
}

adapter::execute(
adapter::execute::<execution_mode::Normal, _, _>(
move_vm,
&mut temporary_store,
ModuleId::new(SUI_FRAMEWORK_ADDRESS, ident_str!("genesis").to_owned()),
Expand Down
35 changes: 32 additions & 3 deletions crates/sui-core/src/authority.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use prometheus::{
exponential_buckets, register_histogram_with_registry, register_int_counter_with_registry,
register_int_gauge_with_registry, Histogram, IntCounter, IntGauge, Registry,
};
use sui_adapter::execution_mode::DevInspect;
use tap::TapFallible;
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};
Expand All @@ -44,7 +45,7 @@ use narwhal_config::{
WorkerId as ConsensusWorkerId,
};
use narwhal_types::CommittedSubDag;
use sui_adapter::adapter;
use sui_adapter::{adapter, execution_mode};
use sui_config::genesis::Genesis;
use sui_json_rpc_types::{
type_and_fields_from_move_struct, SuiEvent, SuiEventEnvelope, SuiTransactionEffects,
Expand Down Expand Up @@ -1042,7 +1043,7 @@ impl AuthorityState {
let temporary_store =
TemporaryStore::new(self.database.clone(), input_objects, *certificate.digest());
let (inner_temp_store, effects, _execution_error) =
execution_engine::execute_transaction_to_effects(
execution_engine::execute_transaction_to_effects::<execution_mode::Normal, _>(
shared_object_refs,
temporary_store,
certificate.data().data.clone(),
Expand Down Expand Up @@ -1074,7 +1075,7 @@ impl AuthorityState {
let temporary_store =
TemporaryStore::new(self.database.clone(), input_objects, transaction_digest);
let (_inner_temp_store, effects, _execution_error) =
execution_engine::execute_transaction_to_effects(
execution_engine::execute_transaction_to_effects::<execution_mode::Normal, _>(
shared_object_refs,
temporary_store,
transaction,
Expand All @@ -1088,6 +1089,34 @@ impl AuthorityState {
SuiTransactionEffects::try_from(effects, self.module_cache.as_ref())
}

pub async fn dev_inspect_transaction(
&self,
transaction: TransactionData,
transaction_digest: TransactionDigest,
) -> Result<DevInspectResults, anyhow::Error> {
let (gas_status, input_objects) =
transaction_input_checker::check_transaction_input(&self.database, &transaction)
.await?;
let shared_object_refs = input_objects.filter_shared_objects();

let transaction_dependencies = input_objects.transaction_dependencies();
let temporary_store =
TemporaryStore::new(self.database.clone(), input_objects, transaction_digest);
let (_inner_temp_store, effects, execution_error) =
execution_engine::execute_transaction_to_effects::<execution_mode::DevInspect, _>(
shared_object_refs,
temporary_store,
transaction,
transaction_digest,
transaction_dependencies,
&self.move_vm,
&self._native_functions,
gas_status,
self.epoch(),
);
DevInspectResults::try_from(effects, self.module_cache.as_ref())
}

pub fn is_tx_already_executed(&self, digest: &TransactionDigest) -> SuiResult<bool> {
self.database.effects_exists(digest)
}
Expand Down
Loading

0 comments on commit 340832a

Please sign in to comment.