diff --git a/crates/sui-adapter/src/adapter.rs b/crates/sui-adapter/src/adapter.rs index a95d18ac7e606..b3b085cf1295b 100644 --- a/crates/sui-adapter/src/adapter.rs +++ b/crates/sui-adapter/src/adapter.rs @@ -997,7 +997,15 @@ fn validate_primitive_arg( // we already checked the type above and struct layout for this type is guaranteed to exist let string_struct_layout = type_layout.unwrap(); + validate_primitive_arg_string(arg, idx, string_struct, string_struct_layout) +} +pub fn validate_primitive_arg_string( + arg: &[u8], + idx: LocalIndex, + string_struct: (&AccountAddress, &IdentStr, &IdentStr), + string_struct_layout: MoveTypeLayout, +) -> Result<(), ExecutionError> { let string_move_value = MoveValue::simple_deserialize(arg, &string_struct_layout).map_err(|_| { ExecutionError::new_with_source( @@ -1458,7 +1466,7 @@ fn missing_unwrapped_msg(id: &ObjectID) -> String { ) } -fn convert_type_argument_error< +pub fn convert_type_argument_error< 'r, E: Debug, S: ResourceResolver + ModuleResolver, diff --git a/crates/sui-adapter/src/programmable_transactions/context.rs b/crates/sui-adapter/src/programmable_transactions/context.rs index b288f0936cb84..09fedea6b09ec 100644 --- a/crates/sui-adapter/src/programmable_transactions/context.rs +++ b/crates/sui-adapter/src/programmable_transactions/context.rs @@ -1,120 +1,111 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use std::{collections::BTreeMap, fmt}; +use std::{ + collections::{BTreeMap, HashMap}, + fmt, + marker::PhantomData, +}; -use move_core_types::resolver::{ModuleResolver, ResourceResolver}; +use move_binary_format::{errors::VMError, file_format::LocalIndex}; use move_vm_runtime::{move_vm::MoveVM, session::Session}; use sui_cost_tables::bytecode_tables::GasStatus; use sui_protocol_config::ProtocolConfig; use sui_types::{ base_types::{ObjectID, SuiAddress, TxContext}, - error::ExecutionError, - messages::{Argument, CallArg, ObjectArg}, + error::{ExecutionError, ExecutionErrorKind}, + messages::{Argument, CallArg, EntryArgumentErrorKind, ObjectArg}, object::Owner, - storage::{ChildObjectResolver, ParentSync, Storage}, + storage::Storage, }; use crate::adapter::new_session; use super::types::*; -pub struct ExecutionContext< - 'vm, - 'state, - 'a, - 'b, - E: fmt::Debug, - S: ResourceResolver - + ModuleResolver - + Storage - + ParentSync - + ChildObjectResolver, -> { +pub struct ExecutionContext<'vm, 'state, 'a, 'b, E: fmt::Debug, S: StorageView> { pub protocol_config: &'a ProtocolConfig, /// The MoveVM - vm: &'vm MoveVM, + pub vm: &'vm MoveVM, /// The global state, used for resolving packages - state_view: &'state S, + pub state_view: &'state S, /// A shared transaction context, contains transaction digest information and manages the /// creation of new object IDs - ctx: &'a mut TxContext, + pub tx_context: &'a mut TxContext, /// The gas status used for metering - gas_status: &'a mut GasStatus<'b>, + pub gas_status: &'a mut GasStatus<'b>, /// The session used for interacting with Move types and calls - session: Option>, + pub session: Session<'state, 'vm, S>, /// Owner meta data for input objects, - object_owner_map: BTreeMap, + _object_owner_map: BTreeMap, + /// Additional transfers not from the Move runtime + additional_transfers: Vec<(/* new owner */ SuiAddress, ObjectValue)>, + // runtime data /// The runtime value for the Gas coin, None if it has been taken/moved - pub gas: Option, + gas: Option, /// The runtime value for the inputs/call args, None if it has been taken/moved - pub inputs: Vec>, + inputs: Vec>, /// The results of a given command. For most commands, the inner vector will have length 1. /// It will only not be 1 for Move calls with multiple return values. /// Inner values are None if taken/moved by-value - pub results: Vec>>, - /// Additional transfers not from the Move runtime - additional_transfers: Vec<(/* new owner */ SuiAddress, ObjectValue)>, + results: Vec>>, + /// Map of arguments that are currently borrowed in this command, true if the borrow is mutable + /// This gets cleared out when new results are pushed, i.e. the end of a command + borrowed: HashMap, + _e: PhantomData, } impl<'vm, 'state, 'a, 'b, E, S> ExecutionContext<'vm, 'state, 'a, 'b, E, S> where E: fmt::Debug, - S: ResourceResolver - + ModuleResolver - + Storage - + ParentSync - + ChildObjectResolver, + S: StorageView, { pub fn new( protocol_config: &'a ProtocolConfig, vm: &'vm MoveVM, state_view: &'state S, - ctx: &'a mut TxContext, + tx_context: &'a mut TxContext, gas_status: &'a mut GasStatus<'b>, gas_coin: ObjectID, inputs: Vec, ) -> Result { - let mut object_owner_map = BTreeMap::new(); + let mut _object_owner_map = BTreeMap::new(); let inputs = inputs .into_iter() .map(|call_arg| { Ok(Some(load_call_arg( state_view, - &mut object_owner_map, + &mut _object_owner_map, call_arg, )?)) }) .collect::>()?; let gas = Some(Value::Object(load_object( state_view, - &mut object_owner_map, + &mut _object_owner_map, + None, gas_coin, )?)); + let session = new_session( + vm, + state_view, + _object_owner_map.clone(), + gas_status.is_metered(), + protocol_config, + ); Ok(Self { protocol_config, vm, state_view, - ctx, + tx_context, gas_status, - session: None, - object_owner_map, + session, + _object_owner_map, gas, inputs, results: vec![], additional_transfers: vec![], - }) - } - - /// Access the session - pub fn session(&mut self) -> &Session<'state, 'vm, S> { - self.session.get_or_insert_with(|| { - new_session( - self.vm, - self.state_view, - self.object_owner_map.clone(), - self.gas_status.is_metered(), - self.protocol_config, - ) + borrowed: HashMap::new(), + _e: PhantomData, }) } @@ -123,7 +114,7 @@ where if true { todo!("update native context set") } - Ok(self.ctx.fresh_id()) + Ok(self.tx_context.fresh_id()) } /// Delete an ID and update the state @@ -134,33 +125,156 @@ where Ok(()) } - pub fn take_args( + pub fn gas_is_taken(&self) -> bool { + self.gas.is_none() + } + + pub fn take_arg( + &mut self, + command_kind: CommandKind<'_>, + arg_idx: usize, + arg: Argument, + ) -> Result { + if matches!(arg, Argument::GasCoin) && !matches!(command_kind, CommandKind::TransferObjects) + { + panic!("cannot take gas") + } + if self.arg_is_borrowed(&arg) { + panic!("taken borrowed value") + } + let val_opt = self.borrow_mut(arg)?; + if val_opt.is_none() { + panic!("taken value") + } + let val = val_opt.take().unwrap(); + if let Value::Object(obj) = &val { + if let Some(Owner::Shared { .. } | Owner::Immutable) = obj.owner { + let error = format!( + "Immutable and shared objects cannot be passed by-value, \ + violation found in argument {}", + arg_idx + ); + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::entry_argument_error( + arg_idx as LocalIndex, + EntryArgumentErrorKind::InvalidObjectByValue, + ), + error, + )); + } + } + V::try_from_value(val) + } + + pub fn borrow_arg_mut( + &mut self, + arg_idx: usize, + arg: Argument, + ) -> Result { + if self.arg_is_borrowed(&arg) { + panic!("mutable args can only be used once in a given command") + } + self.borrowed.insert(arg, /* is_mut */ true); + let val_opt = self.borrow_mut(arg)?; + if val_opt.is_none() { + panic!("taken value") + } + let val = val_opt.take().unwrap(); + if let Value::Object(obj) = &val { + if let Some(Owner::Immutable) = obj.owner { + let error = format!( + "Argument {} is expected to be mutable, immutable object found", + arg_idx + ); + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::entry_argument_error( + arg_idx as LocalIndex, + EntryArgumentErrorKind::InvalidObjectByMuteRef, + ), + error, + )); + } + } + V::try_from_value(val) + } + + pub fn clone_arg( &mut self, - args: Vec, - ) -> Result, ExecutionError> { - args.into_iter() - .map(|arg| self.take_arg::(arg)) - .collect() + _arg_idx: usize, + arg: Argument, + ) -> Result { + if self.arg_is_mut_borrowed(&arg) { + panic!("mutable args can only be used once in a given command") + } + let val_opt = self.borrow_mut(arg)?; + if val_opt.is_none() { + panic!("taken value") + } + let val = val_opt.as_ref().unwrap().clone(); + V::try_from_value(val) } - pub fn take_arg(&mut self, arg: Argument) -> Result { + pub fn borrow_arg( + &mut self, + _arg_idx: usize, + arg: Argument, + ) -> Result { + if self.arg_is_mut_borrowed(&arg) { + panic!("mutable args can only be used once in a given command") + } + self.borrowed.insert(arg, /* is_mut */ false); let val_opt = self.borrow_mut(arg)?; if val_opt.is_none() { panic!("taken value") } - V::try_from_value(val_opt.take().unwrap()) + V::try_from_value(val_opt.as_ref().unwrap().clone()) } pub fn restore_arg(&mut self, arg: Argument, value: Value) -> Result<(), ExecutionError> { - let old_val = self.borrow_mut(arg)?.replace(value); assert_invariant!( - old_val.is_none(), + self.arg_is_mut_borrowed(&arg), + "Should never restore a non-mut borrowed value. \ + The take+restore is an implementation detail of mutable references" + ); + let old_value = self.borrow_mut(arg)?.replace(value); + assert_invariant!( + old_value.is_none(), "Should never restore a non-taken value. \ The take+restore is an implementation detail of mutable references" ); Ok(()) } + pub fn mark_used_in_non_entry_move_call(&mut self, arg: Argument) { + if let Ok(Some(val)) = self.borrow_mut(arg) { + match val { + Value::Object(obj) => obj.used_in_non_entry_move_call = true, + // nothing to do for raw, either it is pure bytes from input and there is nothing + // to change, or it is a Move value and it is never not tainted + Value::Raw(_, _) => (), + } + } + } + + pub fn push_command_results(&mut self, results: Vec) -> Result<(), ExecutionError> { + assert_invariant!( + self.borrowed.values().all(|is_mut| !is_mut), + "all mut borrows should be restored" + ); + // clear borrow state + self.borrowed = HashMap::new(); + self.results.push(results.into_iter().map(Some).collect()); + Ok(()) + } + + fn arg_is_borrowed(&self, arg: &Argument) -> bool { + self.borrowed.contains_key(arg) + } + + fn arg_is_mut_borrowed(&self, arg: &Argument) -> bool { + matches!(self.borrowed.get(arg), Some(/* mut */ true)) + } + fn borrow_mut(&mut self, arg: Argument) -> Result<&mut Option, ExecutionError> { Ok(match arg { Argument::GasCoin => &mut self.gas, @@ -199,21 +313,32 @@ where self.additional_transfers.push((arg, obj)); Ok(()) } + + pub fn convert_vm_error(&self, error: VMError) -> ExecutionError { + sui_types::error::convert_vm_error(error, self.vm, self.state_view) + } } fn load_object( state_view: &S, object_owner_map: &mut BTreeMap, + // used for masking the owner, specifically in the case where a shared object is read-only and + // acts like an immutable object + owner_override: Option, id: ObjectID, ) -> Result { let Some(obj) = state_view.read_object(&id) else { // protected by transaction input checker invariant_violation!(format!("Object {} does not exist yet", id)); }; - let prev = object_owner_map.insert(id, obj.owner); + let owner = owner_override.unwrap_or(obj.owner); + let prev = object_owner_map.insert(id, owner); // protected by transaction input checker assert_invariant!(prev.is_none(), format!("Duplicate input object {}", id)); - ObjectValue::from_object(obj) + let mut obj_value = ObjectValue::from_object(obj)?; + // propagate override + obj_value.owner = Some(owner); + Ok(obj_value) } fn load_call_arg( @@ -239,8 +364,12 @@ fn load_object_arg( obj_arg: ObjectArg, ) -> Result { match obj_arg { - ObjectArg::ImmOrOwnedObject((id, _, _)) | ObjectArg::SharedObject { id, .. } => { - load_object(state_view, object_owner_map, id) - } + ObjectArg::ImmOrOwnedObject((id, _, _)) + | ObjectArg::SharedObject { + id, mutable: true, .. + } => load_object(state_view, object_owner_map, None, id), + ObjectArg::SharedObject { + id, mutable: false, .. + } => load_object(state_view, object_owner_map, Some(Owner::Immutable), id), } } diff --git a/crates/sui-adapter/src/programmable_transactions/execution.rs b/crates/sui-adapter/src/programmable_transactions/execution.rs index de449e5b672df..72d44e0d42fa9 100644 --- a/crates/sui-adapter/src/programmable_transactions/execution.rs +++ b/crates/sui-adapter/src/programmable_transactions/execution.rs @@ -3,30 +3,47 @@ use std::fmt; -use move_core_types::resolver::{ModuleResolver, ResourceResolver}; -use move_vm_runtime::move_vm::MoveVM; +use move_binary_format::{ + access::ModuleAccess, + file_format::{AbilitySet, LocalIndex, Visibility}, +}; +use move_core_types::{ + account_address::AccountAddress, + identifier::{IdentStr, Identifier}, + language_storage::{ModuleId, StructTag, TypeTag}, + value::{MoveStructLayout, MoveTypeLayout}, +}; +use move_vm_runtime::{ + move_vm::MoveVM, + session::{LoadedFunctionInstantiation, SerializedReturnValues}, +}; +use move_vm_types::loaded_data::runtime_types::{StructType, Type}; use sui_cost_tables::bytecode_tables::GasStatus; use sui_protocol_config::ProtocolConfig; use sui_types::{ balance::Balance, - base_types::{ObjectID, SuiAddress, TxContext}, + base_types::{ObjectID, SuiAddress, TxContext, TX_CONTEXT_MODULE_NAME, TX_CONTEXT_STRUCT_NAME}, coin::Coin, - error::ExecutionError, + error::{ExecutionError, ExecutionErrorKind}, id::UID, - messages::{Command, ProgrammableTransaction}, - storage::{ChildObjectResolver, ParentSync, Storage}, + messages::{ + Argument, Command, EntryArgumentErrorKind, ProgrammableMoveCall, ProgrammableTransaction, + }, + object::Owner, + SUI_FRAMEWORK_ADDRESS, +}; +use sui_verifier::{ + entry_points_verifier::{ + TxContextKind, RESOLVED_ASCII_STR, RESOLVED_STD_OPTION, RESOLVED_SUI_ID, RESOLVED_UTF8_STR, + }, + INIT_FN_NAME, }; +use crate::adapter::{convert_type_argument_error, validate_primitive_arg_string}; + use super::{context::*, types::*}; -pub fn execute< - E: fmt::Debug, - S: ResourceResolver - + ModuleResolver - + Storage - + ParentSync - + ChildObjectResolver, ->( +pub fn execute>( protocol_config: &ProtocolConfig, vm: &MoveVM, state_view: &mut S, @@ -51,22 +68,19 @@ pub fn execute< Ok(()) } -fn execute_command< - E: fmt::Debug, - S: ResourceResolver - + ModuleResolver - + Storage - + ParentSync - + ChildObjectResolver, ->( +/// Execute a single command +fn execute_command>( context: &mut ExecutionContext, command: Command, ) -> Result<(), ExecutionError> { - let is_transfer_objects = matches!(command, Command::TransferObjects(_, _)); let results = match command { Command::TransferObjects(objs, addr_arg) => { - let objs: Vec = context.take_args(objs)?; - let addr: SuiAddress = context.take_arg(addr_arg)?; + let objs: Vec = objs + .into_iter() + .enumerate() + .map(|(idx, arg)| context.take_arg(CommandKind::TransferObjects, idx, arg)) + .collect::>()?; + let addr: SuiAddress = context.clone_arg(objs.len(), addr_arg)?; for obj in objs { obj.ensure_public_transfer_eligible()?; context.transfer_object(obj, addr)?; @@ -74,23 +88,27 @@ fn execute_command< vec![] } Command::SplitCoin(coin_arg, amount_arg) => { - let mut obj: ObjectValue = context.take_arg(coin_arg)?; + let mut obj: ObjectValue = context.borrow_arg_mut(0, coin_arg)?; let ObjectContents::Coin(coin) = &mut obj.contents else { panic!("not a coin") }; - let amount: u64 = context.take_arg(amount_arg)?; + let amount: u64 = context.clone_arg(1, amount_arg)?; let new_coin_id = context.fresh_id()?; let new_coin = coin.split_coin(amount, UID::new(new_coin_id))?; let coin_type = obj.type_.clone(); context.restore_arg(coin_arg, Value::Object(obj))?; - vec![Some(Value::Object(ObjectValue::coin(coin_type, new_coin)?))] + vec![Value::Object(ObjectValue::coin(coin_type, new_coin)?)] } Command::MergeCoins(target_arg, coin_args) => { - let mut target: ObjectValue = context.take_arg(target_arg)?; + let mut target: ObjectValue = context.borrow_arg_mut(0, target_arg)?; let ObjectContents::Coin(target_coin) = &mut target.contents else { panic!("not a coin") }; - let coins: Vec = context.take_args(coin_args)?; + let coins: Vec = coin_args + .into_iter() + .enumerate() + .map(|(idx, arg)| context.take_arg(CommandKind::MergeCoins, idx + 1, arg)) + .collect::>()?; for coin in coins { let ObjectContents::Coin(Coin { id, balance }) = coin.contents else { panic!("not a coin") @@ -105,12 +123,593 @@ fn execute_command< context.restore_arg(target_arg, Value::Object(target))?; vec![] } - Command::MoveCall(_) => todo!(), + Command::MoveCall(move_call) => { + let ProgrammableMoveCall { + package, + module, + function, + type_arguments, + arguments, + } = *move_call; + execute_move_call( + context, + package, + module, + function, + type_arguments, + arguments, + )? + } Command::Publish(_) => todo!(), }; - context.results.push(results); - if !is_transfer_objects && context.gas.is_none() { - panic!("todo gas taken error") - } + context.push_command_results(results)?; Ok(()) } + +/// Execute a single Move call +fn execute_move_call>( + context: &mut ExecutionContext, + package: ObjectID, + module: Identifier, + function: Identifier, + type_arguments: Vec, + arguments: Vec, +) -> Result, ExecutionError> { + let module_id = ModuleId::new(package.into(), module); + // check that the function is either an entry function or a valid public function + let (function_kind, signature, return_value_kinds) = check_visibility_and_signature( + context, + &module_id, + &function, + &type_arguments, + /* init */ false, + )?; + // build the arguments, storing meta data about by-mut-ref args + let (tx_context_kind, by_mut_ref, serialized_arguments) = build_move_args( + context, + &module_id, + &function, + function_kind, + &signature, + &arguments, + )?; + // invoke the VM + let SerializedReturnValues { + mutable_reference_outputs, + return_values, + } = vm_move_call( + context, + &module_id, + &function, + type_arguments, + tx_context_kind, + serialized_arguments, + )?; + assert_invariant!( + by_mut_ref.len() == mutable_reference_outputs.len(), + "lost mutable input" + ); + // write back mutable inputs + for ((i1, bytes, _layout), (i2, value_info)) in + mutable_reference_outputs.into_iter().zip(by_mut_ref) + { + assert_invariant!(i1 == i2, "lost mutable input"); + let arg_idx = i1 as usize; + let value = make_value(value_info, bytes, /* return value */ false)?; + context.restore_arg(arguments[arg_idx], value)?; + } + // taint arguments if this function is not an entry function (i.e. just some public function) + // &mut on primitive, non-object values will already have been tainted when updating the value + if function_kind == FunctionKind::NonEntry { + for arg in &arguments { + context.mark_used_in_non_entry_move_call(*arg); + } + } + + assert_invariant!( + return_value_kinds.len() == return_values.len(), + "lost return value" + ); + return_value_kinds + .into_iter() + .zip(return_values) + .map(|(value_info, (bytes, _layout))| { + make_value(value_info, bytes, /* return value */ true) + }) + .collect() +} + +fn make_value( + value_info: ValueKind, + bytes: Vec, + is_return_value: bool, +) -> Result { + Ok(match value_info { + ValueKind::Object { + owner, + type_, + has_public_transfer, + } => Value::Object(ObjectValue::new( + owner, + type_, + has_public_transfer, + is_return_value, + &bytes, + )?), + ValueKind::Raw(ty, abilities) => Value::Raw(ValueType::Loaded { ty, abilities }, bytes), + }) +} + +/*************************************************************************************************** + * Move execution + **************************************************************************************************/ + +fn vm_move_call>( + context: &mut ExecutionContext, + module_id: &ModuleId, + function: &Identifier, + type_arguments: Vec, + tx_context_kind: TxContextKind, + mut serialized_arguments: Vec>, +) -> Result { + match tx_context_kind { + TxContextKind::None => (), + TxContextKind::Mutable | TxContextKind::Immutable => { + serialized_arguments.push(context.tx_context.to_vec()); + } + } + // script visibility checked manually for entry points + let mut result = context + .session + .execute_function_bypass_visibility( + module_id, + function, + type_arguments, + serialized_arguments, + context.gas_status, + ) + .map_err(|e| context.convert_vm_error(e))?; + + // When this function is used during publishing, it + // may be executed several times, with objects being + // created in the Move VM in each Move call. In such + // case, we need to update TxContext value so that it + // reflects what happened each time we call into the + // Move VM (e.g. to account for the number of created + // objects). + if tx_context_kind == TxContextKind::Mutable { + let (_, ctx_bytes, _) = result.mutable_reference_outputs.pop().unwrap(); + let updated_ctx: TxContext = bcs::from_bytes(&ctx_bytes).unwrap(); + context.tx_context.update_state(updated_ctx)?; + } + Ok(result) +} + +/*************************************************************************************************** + * Move signatures + **************************************************************************************************/ + +/// Helper marking what function we are invoking +#[derive(PartialEq, Eq, Clone, Copy)] +enum FunctionKind { + PrivateEntry, + PublicEntry, + NonEntry, + Init, +} + +/// Used to remember type information about a type when resolving the signature +enum ValueKind { + Object { + owner: Option, + type_: StructTag, + has_public_transfer: bool, + }, + Raw(Type, AbilitySet), +} + +/// Checks that the function to be called is either +/// - an entry function +/// - a public function that does not return references +/// - module init (only internal usage) +fn check_visibility_and_signature>( + context: &mut ExecutionContext, + module_id: &ModuleId, + function: &Identifier, + type_arguments: &[TypeTag], + from_init: bool, +) -> Result<(FunctionKind, LoadedFunctionInstantiation, Vec), ExecutionError> { + for (idx, ty) in type_arguments.iter().enumerate() { + context + .session + .load_type(ty) + .map_err(|e| convert_type_argument_error(idx, e, context.vm, context.state_view))?; + } + let function_kind = { + let module = context + .vm + .load_module(module_id, context.state_view) + .map_err(|e| context.convert_vm_error(e))?; + let function_str = function.as_ident_str(); + let module_id = module.self_id(); + let Some(fdef) = module.function_defs.iter().find(|fdef| { + module.identifier_at(module.function_handle_at(fdef.function).name) == function_str + }) else { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::FunctionNotFound, + format!( + "Could not resolve function '{}' in module {}", + function, &module_id, + ), + )); + }; + // TODO dev inspect + match (fdef.visibility, fdef.is_entry) { + (Visibility::Private | Visibility::Friend, true) => FunctionKind::PrivateEntry, + (Visibility::Public, true) => FunctionKind::PublicEntry, + (Visibility::Public, false) => FunctionKind::NonEntry, + (Visibility::Private, false) if from_init => { + assert_invariant!( + function.as_ident_str() == INIT_FN_NAME, + "module init specified non-init function" + ); + FunctionKind::Init + } + (Visibility::Private | Visibility::Friend, false) => { + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::NonEntryFunctionInvoked, + "Can only call `entry` or `public` functions", + )); + } + } + }; + let signature = context + .session + .load_function(module_id, function, type_arguments) + .map_err(|e| context.convert_vm_error(e))?; + let return_value_kinds = match function_kind { + FunctionKind::PrivateEntry | FunctionKind::PublicEntry | FunctionKind::Init => { + assert_invariant!( + signature.return_.is_empty(), + "entry functions must have no return values" + ); + vec![] + } + FunctionKind::NonEntry => { + check_non_entry_signature(context, module_id, function, &signature)? + } + }; + Ok((function_kind, signature, return_value_kinds)) +} + +/// Checks that the non-entry function does not return references. And marks the return values +/// as object or non-object return values +fn check_non_entry_signature>( + context: &mut ExecutionContext, + _module_id: &ModuleId, + _function: &Identifier, + signature: &LoadedFunctionInstantiation, +) -> Result, ExecutionError> { + signature + .return_ + .iter() + .map(|return_type| { + if let Type::Reference(_) | Type::MutableReference(_) = return_type { + panic!("references not supported") + }; + let abilities = context + .session + .get_type_abilities(return_type) + .map_err(|e| context.convert_vm_error(e))?; + Ok(match return_type { + Type::MutableReference(_) | Type::Reference(_) => unreachable!(), + Type::TyParam(_) => invariant_violation!("TyParam should have been substituted"), + Type::Struct(_) | Type::StructInstantiation(_, _) if abilities.has_key() => { + let type_tag = context + .session + .get_type_tag(return_type) + .map_err(|e| context.convert_vm_error(e))?; + let TypeTag::Struct(struct_tag) = type_tag else { + invariant_violation!("Struct type make a non struct type tag") + }; + ValueKind::Object { + owner: None, + type_: *struct_tag, + has_public_transfer: abilities.has_store(), + } + } + Type::Struct(_) + | Type::StructInstantiation(_, _) + | Type::Bool + | Type::U8 + | Type::U64 + | Type::U128 + | Type::Address + | Type::Signer + | Type::Vector(_) + | Type::U16 + | Type::U32 + | Type::U256 => ValueKind::Raw(return_type.clone(), abilities), + }) + }) + .collect() +} + +type ArgInfo = ( + TxContextKind, + /* mut ref */ + Vec<(LocalIndex, ValueKind)>, + Vec>, +); + +/// Serializes the arguments into BCS values for Move. Performs the necessary type checking for +/// each value +fn build_move_args>( + context: &mut ExecutionContext, + module_id: &ModuleId, + function: &Identifier, + function_kind: FunctionKind, + signature: &LoadedFunctionInstantiation, + args: &[Argument], +) -> Result { + // check the arity + let parameters = &signature.parameters; + let tx_ctx_kind = match parameters.last() { + Some(t) => is_tx_context(context, t)?, + None => TxContextKind::None, + }; + let num_args = if tx_ctx_kind != TxContextKind::None { + args.len() + 1 + } else { + args.len() + }; + if num_args != parameters.len() { + let idx = std::cmp::min(parameters.len(), num_args) as LocalIndex; + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::entry_argument_error(idx, EntryArgumentErrorKind::ArityMismatch), + format!( + "Expected {:?} argument{} calling function '{}', but found {:?}", + parameters.len(), + if parameters.len() == 1 { "" } else { "s" }, + function, + num_args + ), + )); + } + + // check the types and remember which are by mutable ref + let mut by_mut_ref = vec![]; + let mut serialized_args = Vec::with_capacity(num_args); + let command_kind = CommandKind::MoveCall { + package: (*module_id.address()).into(), + module: module_id.name(), + function: function.as_ident_str(), + }; + for ((idx, arg), param_ty) in args.iter().copied().enumerate().zip(parameters) { + let (value, non_ref_param_ty): (Value, &Type) = match param_ty { + Type::MutableReference(inner) => { + let value = context.borrow_arg_mut(idx, arg)?; + let object_info = if let Value::Object(ObjectValue { + owner, + type_, + has_public_transfer, + .. + }) = &value + { + ValueKind::Object { + owner: *owner, + type_: type_.clone(), + has_public_transfer: *has_public_transfer, + } + } else { + let abilities = context + .session + .get_type_abilities(inner) + .map_err(|e| context.convert_vm_error(e))?; + ValueKind::Raw((**inner).clone(), abilities) + }; + by_mut_ref.push((idx as LocalIndex, object_info)); + (value, inner) + } + Type::Reference(inner) => (context.borrow_arg(idx, arg)?, inner), + t => { + let abilities = context + .session + .get_type_abilities(t) + .map_err(|e| context.convert_vm_error(e))?; + let value = if abilities.has_copy() { + context.clone_arg(idx, arg)? + } else { + context.take_arg(command_kind, idx, arg)? + }; + (value, t) + } + }; + if function_kind == FunctionKind::PrivateEntry && value.was_used_in_non_entry_move_call() { + panic!("private entry taint failed") + } + check_param_type(context, idx, &value, non_ref_param_ty)?; + let bytes = value.to_bcs_bytes(); + // Any means this was just some bytes passed in as an argument (as opposed to being + // generated from a Move function). Meaning we will need to run validation + if matches!(value, Value::Raw(ValueType::Any, _)) { + if let Some((string_struct, string_struct_layout)) = is_string_arg(context, param_ty)? { + validate_primitive_arg_string( + &bytes, + idx as LocalIndex, + string_struct, + string_struct_layout, + )?; + } + } + serialized_args.push(bytes); + } + Ok((tx_ctx_kind, by_mut_ref, serialized_args)) +} + +/// checks that the value is compatible with the specified type +fn check_param_type>( + context: &mut ExecutionContext, + idx: usize, + value: &Value, + param_ty: &Type, +) -> Result<(), ExecutionError> { + let obj_ty; + let ty = match value { + // TODO dev inspect + Value::Raw(ValueType::Any, _) => { + if !is_entry_primitive_type(context, param_ty)? { + let msg = format!( + "Non-primitive argument at index {}. If it is an object, it must be \ + populated by an object ID", + idx, + ); + return Err(ExecutionError::new_with_source( + ExecutionErrorKind::entry_argument_error( + idx as LocalIndex, + EntryArgumentErrorKind::UnsupportedPureArg, + ), + msg, + )); + } else { + return Ok(()); + } + } + Value::Raw(ValueType::Loaded { ty, abilities }, _) => { + assert_invariant!(!abilities.has_key(), "Raw value should never be an object"); + ty + } + Value::Object(obj) => { + obj_ty = context + .session + .load_type(&TypeTag::Struct(Box::new(obj.type_.clone()))) + .map_err(|e| context.convert_vm_error(e))?; + &obj_ty + } + }; + if ty != param_ty { + panic!("type mismatch") + } else { + Ok(()) + } +} + +/// If the type is a string, returns the name of the string type and the layout +/// Otherwise, returns None +fn is_string_arg>( + context: &mut ExecutionContext, + param_ty: &Type, +) -> Result, ExecutionError> { + let Type::Struct(idx) = param_ty else { return Ok(None) }; + let Some(s) = context.session.get_struct_type(*idx) else { + invariant_violation!("Loaded struct not found") + }; + let resolved_struct = get_struct_ident(&s); + let string_name = if resolved_struct == RESOLVED_ASCII_STR { + RESOLVED_ASCII_STR + } else if resolved_struct == RESOLVED_UTF8_STR { + RESOLVED_UTF8_STR + } else { + return Ok(None); + }; + let layout = MoveTypeLayout::Struct(MoveStructLayout::Runtime(vec![MoveTypeLayout::Vector( + Box::new(MoveTypeLayout::U8), + )])); + Ok(Some((string_name, layout))) +} +type StringInfo = ( + ( + &'static AccountAddress, + &'static IdentStr, + &'static IdentStr, + ), + MoveTypeLayout, +); + +// Returns Some(kind) if the type is a reference to the TxnContext. kind being Mutable with +// a MutableReference, and Immutable otherwise. +// Returns None for all other types +pub fn is_tx_context>( + context: &mut ExecutionContext, + t: &Type, +) -> Result { + let (is_mut, inner) = match t { + Type::MutableReference(inner) => (true, inner), + Type::Reference(inner) => (false, inner), + _ => return Ok(TxContextKind::None), + }; + let Type::Struct(idx) = &**inner else { return Ok(TxContextKind::None) }; + let Some(s) = context.session.get_struct_type(*idx) else { + invariant_violation!("Loaded struct not found") + }; + let (module_addr, module_name, struct_name) = get_struct_ident(&s); + let is_tx_context_type = module_addr == &SUI_FRAMEWORK_ADDRESS + && module_name == TX_CONTEXT_MODULE_NAME + && struct_name == TX_CONTEXT_STRUCT_NAME; + Ok(if is_tx_context_type { + if is_mut { + TxContextKind::Mutable + } else { + TxContextKind::Immutable + } + } else { + TxContextKind::None + }) +} + +/// Returns true iff it is a primitive, an ID, a String, or an option/vector of a valid type +fn is_entry_primitive_type>( + context: &mut ExecutionContext, + param_ty: &Type, +) -> Result { + let mut stack = vec![param_ty]; + while let Some(cur) = stack.pop() { + match cur { + Type::Signer => return Ok(false), + Type::Reference(_) | Type::MutableReference(_) | Type::TyParam(_) => return Ok(false), + Type::Bool + | Type::U8 + | Type::U16 + | Type::U32 + | Type::U64 + | Type::U128 + | Type::U256 + | Type::Address => (), + Type::Vector(inner) => stack.push(&**inner), + Type::Struct(idx) => { + let Some(s) = context.session.get_struct_type(*idx) else { + invariant_violation!("Loaded struct not found") + }; + let resolved_struct = get_struct_ident(&s); + if ![RESOLVED_SUI_ID, RESOLVED_ASCII_STR, RESOLVED_UTF8_STR] + .contains(&resolved_struct) + { + return Ok(false); + } + } + Type::StructInstantiation(idx, targs) => { + let Some(s) = context.session.get_struct_type(*idx) else { + invariant_violation!("Loaded struct not found") + }; + let resolved_struct = get_struct_ident(&s); + // is option of a primitive + let is_valid = resolved_struct == RESOLVED_STD_OPTION && targs.len() == 1; + if !is_valid { + return Ok(false); + } + stack.extend(targs) + } + } + } + Ok(true) +} + +fn get_struct_ident(s: &StructType) -> (&AccountAddress, &IdentStr, &IdentStr) { + let module_id = &s.module; + let struct_name = &s.name; + ( + module_id.address(), + module_id.name(), + struct_name.as_ident_str(), + ) +} diff --git a/crates/sui-adapter/src/programmable_transactions/types.rs b/crates/sui-adapter/src/programmable_transactions/types.rs index 77419808f71fc..b64b54271cd87 100644 --- a/crates/sui-adapter/src/programmable_transactions/types.rs +++ b/crates/sui-adapter/src/programmable_transactions/types.rs @@ -2,74 +2,154 @@ // SPDX-License-Identifier: Apache-2.0 use move_binary_format::file_format::AbilitySet; -use move_core_types::language_storage::StructTag; +use move_core_types::{ + identifier::IdentStr, + language_storage::StructTag, + resolver::{ModuleResolver, ResourceResolver}, +}; use move_vm_types::loaded_data::runtime_types::Type; use serde::Deserialize; use sui_types::{ - base_types::SuiAddress, + base_types::{ObjectID, SuiAddress}, coin::Coin, error::{ExecutionError, ExecutionErrorKind}, object::{Data, MoveObject, Object, Owner}, + storage::{ChildObjectResolver, ParentSync, Storage}, }; +pub trait StorageView: + ResourceResolver + ModuleResolver + Storage + ParentSync + ChildObjectResolver +{ +} +impl< + E: std::fmt::Debug, + T: ResourceResolver + + ModuleResolver + + Storage + + ParentSync + + ChildObjectResolver, + > StorageView for T +{ +} + +#[derive(Clone)] pub enum Value { Object(ObjectValue), Raw(ValueType, Vec), } +#[derive(Clone)] pub struct ObjectValue { - pub type_: StructTag, - pub has_public_transfer: bool, // None for objects created this transaction pub owner: Option, - // true if it has been used in a public Move call + pub type_: StructTag, + pub has_public_transfer: bool, + // true if it has been used in a public, non-entry Move call // In other words, false if all usages have been with non-Move comamnds or // entry Move functions - pub used_in_public_move_call: bool, + pub used_in_non_entry_move_call: bool, pub contents: ObjectContents, } +#[derive(Clone)] pub enum ObjectContents { Coin(Coin), Raw(Vec), } +#[derive(Clone)] pub enum ValueType { Any, Loaded { ty: Type, abilities: AbilitySet }, } -impl Value {} +#[derive(Clone, Copy)] +pub enum CommandKind<'a> { + MoveCall { + package: ObjectID, + module: &'a IdentStr, + function: &'a IdentStr, + }, + TransferObjects, + SplitCoin, + MergeCoins, + Publish, +} -impl ObjectValue { - pub fn from_object(object: &Object) -> Result { - let Object { data, owner, .. } = object; - match data { - Data::Package(_) => invariant_violation!("Expected a Move object"), - Data::Move(move_object) => Self::from_move_object(*owner, move_object), +impl Value { + pub fn is_copyable(&self) -> bool { + match self { + Value::Object(_) => false, + Value::Raw(ValueType::Any, _) => true, + Value::Raw(ValueType::Loaded { abilities, .. }, _) => abilities.has_copy(), } } - pub fn from_move_object(owner: Owner, object: &MoveObject) -> Result { - let type_ = object.type_.clone(); - let has_public_transfer = object.has_public_transfer(); - let contents = object.contents(); - let owner = Some(owner); - let used_in_public_move_call = false; + pub fn to_bcs_bytes(&self) -> Vec { + match self { + Value::Object(ObjectValue { + contents: ObjectContents::Raw(bytes), + .. + }) + | Value::Raw(_, bytes) => bytes.clone(), + Value::Object(ObjectValue { + contents: ObjectContents::Coin(coin), + .. + }) => coin.to_bcs_bytes(), + } + } + + pub fn was_used_in_non_entry_move_call(&self) -> bool { + match self { + Value::Object(obj) => obj.used_in_non_entry_move_call, + // Any is only used for Pure inputs, and if it was used by &mut it would have switched + // to Loaded + Value::Raw(ValueType::Any, _) => false, + Value::Raw(ValueType::Loaded { .. }, _) => true, + } + } +} + +impl ObjectValue { + pub fn new( + owner: Option, + type_: StructTag, + has_public_transfer: bool, + used_in_non_entry_move_call: bool, + contents: &[u8], + ) -> Result { let contents = if Coin::is_coin(&type_) { ObjectContents::Coin(Coin::from_bcs_bytes(contents)?) } else { ObjectContents::Raw(contents.to_vec()) }; Ok(Self { + owner, type_, has_public_transfer, - owner, - used_in_public_move_call, + used_in_non_entry_move_call, contents, }) } + pub fn from_object(object: &Object) -> Result { + let Object { data, owner, .. } = object; + match data { + Data::Package(_) => invariant_violation!("Expected a Move object"), + Data::Move(move_object) => Self::from_move_object(*owner, move_object), + } + } + + pub fn from_move_object(owner: Owner, object: &MoveObject) -> Result { + Self::new( + Some(owner), + object.type_.clone(), + object.has_public_transfer(), + false, + object.contents(), + ) + } + pub fn coin(type_: StructTag, coin: Coin) -> Result { assert_invariant!( Coin::is_coin(&type_), @@ -79,7 +159,7 @@ impl ObjectValue { type_, has_public_transfer: true, owner: None, - used_in_public_move_call: false, + used_in_non_entry_move_call: false, contents: ObjectContents::Coin(coin), }) } @@ -112,11 +192,8 @@ impl TryFromValue for ObjectValue { fn try_from_value(value: Value) -> Result { match value { Value::Object(o) => Ok(o), - Value::Raw(ty, _) => { - if let ValueType::Loaded { abilities, .. } = ty { - assert_invariant!(!abilities.has_key(), "Raw values should not be objects") - } - panic!("expected object") + Value::Raw(_, _) => { + todo!("support this for dev inspect") } } }