From aa7658a5e5e204405b60b61ab09619462f845499 Mon Sep 17 00:00:00 2001 From: stevenlaw123 <141568913+stevenlaw123@users.noreply.github.com> Date: Wed, 25 Oct 2023 18:09:59 +0800 Subject: [PATCH] Check the Gas Free function during the compilation of the Move module. (#886) * Check the Gas Free function during the compilation of the Move module. * Check the gas_free function during module publishing. * fix the ordering of verifier function * execute the gas_validate and gas_charge_post function when use tx has been executed * Add gas_free function to deduct module account fees and update test code. * Fix error handling and code reuse. * Report an error indicating that the definition of the gas_free function is incomplete. * Fix the checking logic. * Resolve the issue with executing the gas_free callback function. --------- Co-authored-by: stevenlaw123 --- .../sources/transaction_validator.move | 7 +- examples/gas_free/Move.toml | 14 + examples/gas_free/sources/gas_free.move | 15 + .../moveos-stdlib/doc/tx_result.md | 31 ++ .../moveos-stdlib/sources/tx_result.move | 6 + .../moveos-types/src/moveos_std/tx_result.rs | 5 +- moveos/moveos-verifier/src/metadata.rs | 353 +++++++++++++++++- moveos/moveos-verifier/src/verifier.rs | 339 ++++++++++++++++- moveos/moveos/src/moveos.rs | 197 +++++++++- 9 files changed, 955 insertions(+), 12 deletions(-) create mode 100644 examples/gas_free/Move.toml create mode 100644 examples/gas_free/sources/gas_free.move diff --git a/crates/rooch-framework/sources/transaction_validator.move b/crates/rooch-framework/sources/transaction_validator.move index 53bcb63bc2..a899e4ea84 100644 --- a/crates/rooch-framework/sources/transaction_validator.move +++ b/crates/rooch-framework/sources/transaction_validator.move @@ -149,7 +149,7 @@ module rooch_framework::transaction_validator { fun post_execute( ctx: &mut Context, ) { - let sender = context::sender(ctx); + // let sender = context::sender(ctx); // Active the session key @@ -161,11 +161,12 @@ module rooch_framework::transaction_validator { // Increment sequence number account::increment_sequence_number(ctx); - + let tx_result = context::tx_result(ctx); + let gas_payment_account = tx_result::gas_payment_account(&tx_result); let gas_used = tx_result::gas_used(&tx_result); let gas = transaction_fee::calculate_gas(ctx, gas_used); - let gas_coin = gas_coin::deduct_gas(ctx, sender, gas); + let gas_coin = gas_coin::deduct_gas(ctx, gas_payment_account, gas); transaction_fee::deposit_fee(ctx, gas_coin); } } diff --git a/examples/gas_free/Move.toml b/examples/gas_free/Move.toml new file mode 100644 index 0000000000..6b932b83a9 --- /dev/null +++ b/examples/gas_free/Move.toml @@ -0,0 +1,14 @@ +[package] +name = "gas_payer" +version = "0.0.1" + +[dependencies] +MoveosStdlib = { local = "../../moveos/moveos-stdlib/moveos-stdlib" } + +[addresses] +rooch_examples = "_" +moveos_std = "0x2" +rooch_framework = "0x3" + +[dev-addresses] +rooch_examples = "0x42" diff --git a/examples/gas_free/sources/gas_free.move b/examples/gas_free/sources/gas_free.move new file mode 100644 index 0000000000..0134a04923 --- /dev/null +++ b/examples/gas_free/sources/gas_free.move @@ -0,0 +1,15 @@ +module rooch_examples::gas_payer { + use moveos_std::context::Context; + + fun gas_validate_function(_ctx: &Context): bool{ + true + } + + fun gas_charge_post_function(_ctx: &mut Context, _gas_used: u128): bool { + true + } + + #[gas_free(gas_validate=gas_validate_function, gas_charge_post=gas_charge_post_function)] + public entry fun play(_ctx: &mut Context, _sender: &signer){ + } +} diff --git a/moveos/moveos-stdlib/moveos-stdlib/doc/tx_result.md b/moveos/moveos-stdlib/moveos-stdlib/doc/tx_result.md index be355d89ed..6e06b683f6 100644 --- a/moveos/moveos-stdlib/moveos-stdlib/doc/tx_result.md +++ b/moveos/moveos-stdlib/moveos-stdlib/doc/tx_result.md @@ -8,6 +8,7 @@ - [Struct `TxResult`](#0x2_tx_result_TxResult) - [Function `is_executed`](#0x2_tx_result_is_executed) - [Function `gas_used`](#0x2_tx_result_gas_used) +- [Function `gas_payment_account`](#0x2_tx_result_gas_payment_account)
@@ -45,6 +46,12 @@ We can get the result in the post_execute function.
The gas used by the transaction.
+
+gas_payment_account: address +
+
+ The account for the gas payment. +
@@ -96,4 +103,28 @@ We can get the result in the post_execute function. + + + + +## Function `gas_payment_account` + + + +
public fun gas_payment_account(self: &tx_result::TxResult): address
+
+ + + +
+Implementation + + +
public fun gas_payment_account(self: &TxResult): address {
+    self.gas_payment_account
+}
+
+ + +
diff --git a/moveos/moveos-stdlib/moveos-stdlib/sources/tx_result.move b/moveos/moveos-stdlib/moveos-stdlib/sources/tx_result.move index 73ef6d0da5..2c93512137 100644 --- a/moveos/moveos-stdlib/moveos-stdlib/sources/tx_result.move +++ b/moveos/moveos-stdlib/moveos-stdlib/sources/tx_result.move @@ -11,6 +11,8 @@ module moveos_std::tx_result { executed: bool, /// The gas used by the transaction. gas_used: u64, + /// The account for the gas payment. + gas_payment_account: address, } public fun is_executed(self: &TxResult) : bool { @@ -21,4 +23,8 @@ module moveos_std::tx_result { self.gas_used } + public fun gas_payment_account(self: &TxResult): address { + self.gas_payment_account + } + } \ No newline at end of file diff --git a/moveos/moveos-types/src/moveos_std/tx_result.rs b/moveos/moveos-types/src/moveos_std/tx_result.rs index e3ca5b4f41..f02b97839e 100644 --- a/moveos/moveos-types/src/moveos_std/tx_result.rs +++ b/moveos/moveos-types/src/moveos_std/tx_result.rs @@ -16,6 +16,7 @@ pub const MODULE_NAME: &IdentStr = ident_str!("tx_result"); pub struct TxResult { pub executed: bool, pub gas_used: u64, + pub gas_payment_account: AccountAddress, } impl MoveStructType for TxResult { @@ -29,15 +30,17 @@ impl MoveStructState for TxResult { move_core_types::value::MoveStructLayout::new(vec![ move_core_types::value::MoveTypeLayout::Bool, move_core_types::value::MoveTypeLayout::U64, + move_core_types::value::MoveTypeLayout::Address, ]) } } impl TxResult { - pub fn new(status: &KeptVMStatus, gas_used: u64) -> Self { + pub fn new(status: &KeptVMStatus, gas_used: u64, gas_payment_account: AccountAddress) -> Self { Self { executed: matches!(status, KeptVMStatus::Executed), gas_used, + gas_payment_account, } } } diff --git a/moveos/moveos-verifier/src/metadata.rs b/moveos/moveos-verifier/src/metadata.rs index 6208307cf1..97c0617da3 100644 --- a/moveos/moveos-verifier/src/metadata.rs +++ b/moveos/moveos-verifier/src/metadata.rs @@ -7,13 +7,15 @@ use crate::build::ROOCH_METADATA_KEY; use crate::verifier::INIT_FN_NAME_IDENTIFIER; use move_binary_format::binary_views::BinaryIndexedView; +use move_binary_format::errors::{Location, PartialVMError, VMResult}; use move_binary_format::file_format::{ Bytecode, FunctionInstantiation, SignatureToken, Visibility, }; use move_binary_format::CompiledModule; use move_core_types::language_storage::ModuleId; use move_core_types::metadata::Metadata; -use move_model::ast::Attribute; +use move_core_types::vm_status::StatusCode; +use move_model::ast::{Attribute, AttributeValue}; use move_model::model::{FunctionEnv, GlobalEnv, Loc, ModuleEnv}; use move_model::ty::PrimitiveType; use move_model::ty::Type; @@ -29,8 +31,15 @@ use thiserror::Error; pub static mut GLOBAL_PRIVATE_GENERICS: Lazy>> = Lazy::new(|| BTreeMap::new()); +pub static mut GLOBAL_GAS_FREE_RECORDER: Lazy>> = + Lazy::new(|| BTreeMap::new()); + const PRIVATE_GENERICS_ATTRIBUTE: &str = "private_generics"; +const GAS_FREE_ATTRIBUTE: &str = "gas_free"; +const GAS_FREE_VALIDATE: &str = "gas_validate"; +const GAS_FREE_CHARGE_POST: &str = "gas_charge_post"; + /// Enumeration of potentially known attributes #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct KnownAttribute { @@ -38,6 +47,12 @@ pub struct KnownAttribute { args: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] +pub struct GasFreeFunction { + pub gas_validate: String, + pub gas_charge_post: String, +} + /// V1 of Aptos specific metadata attached to the metadata section of file_format. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct RuntimeModuleMetadataV1 { @@ -49,6 +64,9 @@ pub struct RuntimeModuleMetadataV1 { /// The correspondence between private generics and their type parameters. pub private_generics_indices: BTreeMap>, + + /// Save information for the gas free function. + pub gas_free_function_map: BTreeMap, } impl RuntimeModuleMetadataV1 { @@ -56,6 +74,7 @@ impl RuntimeModuleMetadataV1 { self.fun_attributes.is_empty() && self.struct_attributes.is_empty() && self.private_generics_indices.is_empty() + && self.gas_free_function_map.is_empty() } } @@ -105,6 +124,7 @@ impl<'a> ExtendedChecker<'a> { self.check_entry_functions(module); self.check_init_module(module); self.check_global_storage_access(module); + self.check_gas_free_function(module); } } } @@ -580,6 +600,311 @@ impl<'a> ExtendedChecker<'a> { } } +impl<'a> ExtendedChecker<'a> { + fn check_gas_free_function(&mut self, module: &ModuleEnv) { + for fenv in module.get_functions() { + if self.has_attribute(&fenv, GAS_FREE_ATTRIBUTE) { + let attributes = fenv.get_attributes(); + let mut attribute_gas_validate_found = false; + let mut attribute_gas_charge_post_found = false; + + let mut gas_free_function_map: BTreeMap = BTreeMap::new(); + + // verify and save functions with the attribute #[gas_free] + for attr in attributes { + if let Attribute::Apply(_, _, attribute_list) = attr { + for assign_attribute in attribute_list { + if let Attribute::Assign(_, symbol, attribute_value) = assign_attribute + { + let attribute_key = + module.symbol_pool().string(*symbol).to_string(); + + if attribute_key == GAS_FREE_VALIDATE { + if attribute_gas_validate_found { + self.env.error( + &fenv.get_loc(), + "duplicate attribute key 'gas_validate'", + ) + } else { + attribute_gas_validate_found = true; + } + } + + if attribute_key == GAS_FREE_CHARGE_POST { + if attribute_gas_charge_post_found { + self.env.error( + &fenv.get_loc(), + "duplicate attribute key 'gas_charge_post'", + ) + } else { + attribute_gas_charge_post_found = true; + } + } + + if let AttributeValue::Name( + _, + module_name_opt, + function_name_symbol, + ) = attribute_value + { + // if there is no module name specified by user + // compiler will use current module as the module name. + match module_name_opt { + None => { + let gas_function_name = module + .symbol_pool() + .string(*function_name_symbol) + .to_string(); + + if let Some(gas_function) = + get_module_env_function(module, &gas_function_name) + { + let current_module = + module.self_address().to_hex_literal(); + let current_module_name = fenv + .symbol_pool() + .string(fenv.module_env.get_name().name()) + .to_string(); + let full_function_name = format!( + "{}::{}::{}", + current_module, + current_module_name, + gas_function_name + ); + + let current_module = + fenv.module_env.self_address().to_hex_literal(); + let current_module_name = fenv + .symbol_pool() + .string(fenv.module_env.get_name().name()) + .to_string(); + let current_function_name = format!( + "{}::{}::{}", + current_module, + current_module_name, + module.symbol_pool().string(fenv.get_name()) + ); + + let gas_free_function_info = gas_free_function_map + .entry(current_function_name) + .or_default(); + + if attribute_key == GAS_FREE_VALIDATE { + let (is_ok, error_msg) = + check_gas_validate_function( + &gas_function, + self.env, + ); + if !is_ok { + self.env.error( + &fenv.get_loc(), + error_msg.as_str(), + ); + } + gas_free_function_info.gas_validate = + full_function_name.clone(); + } + + if attribute_key == GAS_FREE_CHARGE_POST { + let (is_ok, error_msg) = + check_gas_charge_post_function( + &gas_function, + self.env, + ); + if !is_ok { + self.env.error( + &fenv.get_loc(), + error_msg.as_str(), + ); + } + gas_free_function_info.gas_charge_post = + full_function_name; + } + } else { + self.env.error(&fenv.get_loc(), format!("Gas function {:?} is not found in current module.", gas_function_name).as_str()); + } + } + + Some(module_name_ref) => { + let gas_function_name = module + .symbol_pool() + .string(*function_name_symbol) + .to_string(); + if module_name_ref != module.get_name() { + self.env.error(&fenv.get_loc(), format!("Gas function {:?} is not found in current module.", gas_function_name).as_str()); + } + + let current_module = + fenv.module_env.self_address().to_hex_literal(); + let current_module_name = fenv + .symbol_pool() + .string(fenv.module_env.get_name().name()) + .to_string(); + let function_name = module + .symbol_pool() + .string(*function_name_symbol) + .to_string(); + let full_function_name = format!( + "{}::{}::{}", + current_module, current_module_name, function_name + ); + + let current_module = + fenv.module_env.get_full_name_str(); + let current_function_name = format!( + "{}::{}", + current_module, + module.symbol_pool().string(fenv.get_name()) + ); + + let gas_free_function_info = gas_free_function_map + .entry(current_function_name) + .or_default(); + + if attribute_key == GAS_FREE_VALIDATE { + gas_free_function_info.gas_validate = + full_function_name.clone(); + } + if attribute_key == GAS_FREE_CHARGE_POST { + gas_free_function_info.gas_charge_post = + full_function_name.clone(); + } + } + } + } + } + } + } + } + + if !attribute_gas_validate_found || !attribute_gas_charge_post_found { + self.env.error(&fenv.get_loc(), + format!("The gas_validate function or gas_charge_post function for the {} function was not found.", + module.symbol_pool().string(fenv.get_name())).as_str()); + } + + let module_metadata = self + .output + .entry(module.get_verified_module().self_id()) + .or_default(); + module_metadata.gas_free_function_map = gas_free_function_map; + } + } + } +} + +fn get_module_env_function<'a>( + module_env: &'a ModuleEnv<'a>, + fname: &String, +) -> Option> { + for fenv in module_env.get_functions() { + let function_name = module_env.symbol_pool().string(fenv.get_name()).to_string(); + if &function_name == fname { + return Some(fenv); + } + } + None +} + +fn check_gas_validate_function(fenv: &FunctionEnv, global_env: &GlobalEnv) -> (bool, String) { + let params_types = fenv.get_parameter_types(); + let return_types = fenv.get_return_types(); + if params_types.is_empty() { + return (false, "parameter length is less than 1".to_string()); + } + if return_types.is_empty() { + return (false, "return value length is less than 1".to_string()); + } + + let storage_ctx_type = params_types.get(0).unwrap(); + let parameter_checking_result = match storage_ctx_type { + Type::Struct(module_id, struct_id, _) => { + let struct_name = global_env + .get_struct(module_id.qualified(*struct_id)) + .get_full_name_str(); + if struct_name != "0x2::context::Context" { + ( + false, + format!( + "Type {} cannot be used as the first parameter.", + struct_name + ), + ) + } else { + (true, "".to_string()) + } + } + _ => ( + false, + "Only type 0x2::storage_context::StorageContext can be used as the first parameter." + .to_string(), + ), + }; + + if !parameter_checking_result.0 { + return parameter_checking_result; + } + + let first_return_type = return_types.get(0).unwrap(); + match first_return_type { + Type::Primitive(PrimitiveType::Bool) => (true, "".to_string()), + _ => (false, "Return type must be of type Bool.".to_string()), + } +} + +fn check_gas_charge_post_function(fenv: &FunctionEnv, global_env: &GlobalEnv) -> (bool, String) { + let params_types = fenv.get_parameter_types(); + let return_types = fenv.get_return_types(); + if params_types.len() < 2 { + return (false, "Length of parameters is less than 2.".to_string()); + } + if return_types.is_empty() { + return (false, "Length of return values is less than 1.".to_string()); + } + + let storage_ctx_type = params_types.get(0).unwrap(); + match storage_ctx_type { + Type::Struct(module_id, struct_id, _) => { + let struct_name = global_env + .get_struct(module_id.qualified(*struct_id)) + .get_full_name_str(); + if struct_name != "0x2::storage_context::StorageContext" { + return ( + false, + format!( + "Type {} cannot be used as the first parameter.", + struct_name + ), + ); + } + } + _ => return ( + false, + "Only type 0x2::storage_context::StorageContext can be used as the first parameter." + .to_string(), + ), + } + + let gas_used_type = params_types.get(1).unwrap(); + let second_parameter_checking_result = match *gas_used_type { + Type::Primitive(PrimitiveType::U256) => (true, "".to_string()), + _ => ( + false, + "The second parameter must be of type U256.".to_string(), + ), + }; + + if !second_parameter_checking_result.0 { + return second_parameter_checking_result; + } + + let first_return_type = return_types.get(0).unwrap(); + match first_return_type { + Type::Primitive(PrimitiveType::Bool) => (true, "".to_string()), + _ => (false, "Return type must be of type Bool.".to_string()), + } +} + // ---------------------------------------------------------------------------------- // Helpers @@ -662,3 +987,29 @@ pub fn check_metadata_format(module: &CompiledModule) -> Result<(), MalformedErr Ok(()) } + +pub fn load_module_metadata( + module_id: &ModuleId, + loaded_module_bytes: VMResult>, +) -> VMResult> { + let compiled_module_opt = { + match loaded_module_bytes { + Ok(module_bytes) => CompiledModule::deserialize(module_bytes.as_slice()).ok(), + Err(err) => { + return Err(err); + } + } + }; + + match compiled_module_opt { + None => Err( + PartialVMError::new(StatusCode::FAILED_TO_DESERIALIZE_RESOURCE) + .with_message(format!( + "failed to deserialize module {:?}", + module_id.to_string() + )) + .finish(Location::Module(module_id.clone())), + ), + Some(compiled_module) => Ok(get_metadata_from_compiled_module(&compiled_module)), + } +} diff --git a/moveos/moveos-verifier/src/verifier.rs b/moveos/moveos-verifier/src/verifier.rs index db42c410d0..7c825516ab 100644 --- a/moveos/moveos-verifier/src/verifier.rs +++ b/moveos/moveos-verifier/src/verifier.rs @@ -8,8 +8,9 @@ use crate::metadata::{ use move_binary_format::binary_views::BinaryIndexedView; use move_binary_format::errors::{Location, PartialVMError, PartialVMResult, VMError, VMResult}; use move_binary_format::file_format::{ - Bytecode, FunctionDefinition, FunctionDefinitionIndex, FunctionInstantiation, - FunctionInstantiationIndex, SignatureToken, StructHandleIndex, Visibility, + Bytecode, FunctionDefinition, FunctionDefinitionIndex, FunctionHandleIndex, + FunctionInstantiation, FunctionInstantiationIndex, Signature, SignatureToken, + StructHandleIndex, Visibility, }; use move_binary_format::{access::ModuleAccess, CompiledModule}; use move_core_types::identifier::Identifier; @@ -32,6 +33,7 @@ where verify_private_generics(module, &db)?; verify_entry_function_at_publish(module)?; verify_global_storage_access(module)?; + verify_gas_free_function(module)?; verify_init_function(module) } @@ -465,6 +467,339 @@ where Ok(true) } +pub fn verify_gas_free_function(module: &CompiledModule) -> VMResult { + if let Err(err) = check_metadata_format(module) { + return Err(PartialVMError::new(StatusCode::MALFORMED) + .with_message(err.to_string()) + .finish(Location::Module(module.self_id()))); + } + + let metadata_opt = get_metadata_from_compiled_module(module); + match metadata_opt { + None => { + // If ROOCH_METADATA_KEY cannot be found in the metadata, + // it means that the user's code did not use #[private_generics(T)], + // or the user intentionally deleted the data in the metadata. + // In either case, we will skip the verification. + return Ok(true); + } + + Some(metadata) => { + let gas_free_functions = metadata.gas_free_function_map; + let view = BinaryIndexedView::Module(module); + + for (gas_free_function, gas_function_def) in gas_free_functions.iter() { + // check the existence of the #[gas_free] function, if not we will return failed info. + // The existence means that the #[gas_free] function must be defined in current module. + let (func_exists, func_handle_index) = + check_if_function_exist(module, gas_free_function); + + if !func_exists { + let full_path_module_name = generate_full_module_name(func_handle_index, view); + + let err_msg = format!( + "#[gas_free] function {:?} not defined in module {:?}", + gas_free_function, full_path_module_name + ); + + return generate_vm_error( + StatusCode::RESOURCE_DOES_NOT_EXIST, + err_msg, + func_handle_index, + module, + ); + } + + // check the existence of the 'gas validate' function, if not we will return failed info. + let gas_validate_function = gas_function_def.gas_validate.clone(); + let (func_exists, func_handle_index) = + check_if_function_exist(module, &gas_validate_function); + if !func_exists { + let full_path_module_name = generate_full_module_name(func_handle_index, view); + + let err_msg = format!( + "gas_validate function {:?} not defined in module {:?}", + gas_validate_function, full_path_module_name + ); + + return generate_vm_error( + StatusCode::RESOURCE_DOES_NOT_EXIST, + err_msg, + func_handle_index, + module, + ); + } + + // check if the parameters and return types of the 'gas validate' function are legally. + let func_handle = view.function_handle_at(func_handle_index); + let func_parameters_index = func_handle.parameters; + let func_signature = view.signature_at(func_parameters_index); + let return_type_index = func_handle.return_; + let return_signature = view.signature_at(return_type_index); + + if func_signature.is_empty() || return_signature.is_empty() { + let full_path_module_name = generate_full_module_name(func_handle_index, view); + + let err_msg = format!( + "function {:?} in module {:?} with empty arguments or empty return value.", + gas_validate_function, full_path_module_name + ); + + return generate_vm_error( + StatusCode::RESOURCE_DOES_NOT_EXIST, + err_msg, + func_handle_index, + module, + ); + } + + if func_signature.len() != 1 && return_signature.len() != 1 { + let full_path_module_name = generate_full_module_name(func_handle_index, view); + + let err_msg = format!( + "function {:?} in module {:?} with incorrect number of parameters or return values.", + gas_validate_function, full_path_module_name + ); + + return generate_vm_error( + StatusCode::TOO_MANY_PARAMETERS, + err_msg, + func_handle_index, + module, + ); + } + + let parameter_allowed = + check_gas_validate_function(&view, func_signature, return_signature); + if !parameter_allowed { + let full_path_module_name = generate_full_module_name(func_handle_index, view); + + let err_msg = format!( + "gas validate function {:?} in module {:?} has incorrect parameter type or return type.", + gas_validate_function, full_path_module_name + ); + return generate_vm_error( + StatusCode::TYPE_MISMATCH, + err_msg, + func_handle_index, + module, + ); + } + + // check the existence of the 'gas charge post' function, if not we will return failed info. + // check if the parameters and return types of the 'gas charge' function are legally. + let gas_charge_post_function = gas_function_def.gas_charge_post.clone(); + let (func_exists, func_handle_index) = + check_if_function_exist(module, &gas_charge_post_function); + if !func_exists { + let full_path_module_name = generate_full_module_name(func_handle_index, view); + + let err_msg = format!( + "gas_validate function {:?} not defined in module {:?}", + gas_charge_post_function, full_path_module_name + ); + + return generate_vm_error( + StatusCode::RESOURCE_DOES_NOT_EXIST, + err_msg, + func_handle_index, + module, + ); + } + + // check if the parameters and return types of the 'gas validate' function are legally. + let func_handle = view.function_handle_at(func_handle_index); + let func_parameters_index = func_handle.parameters; + let func_signature = view.signature_at(func_parameters_index); + let return_type_index = func_handle.return_; + let return_signature = view.signature_at(return_type_index); + + if func_signature.is_empty() || return_signature.is_empty() { + let full_path_module_name = generate_full_module_name(func_handle_index, view); + + let err_msg = format!( + "function {:?} in module {:?} with empty arguments or empty return value.", + gas_validate_function, full_path_module_name + ); + + return generate_vm_error( + StatusCode::RESOURCE_DOES_NOT_EXIST, + err_msg, + func_handle_index, + module, + ); + } + + if func_signature.len() != 2 || return_signature.len() != 1 { + let full_path_module_name = generate_full_module_name(func_handle_index, view); + + let err_msg = format!( + "function {:?} in module {:?} with incorrect number of parameters or return values.", + gas_validate_function, full_path_module_name + ); + + return generate_vm_error( + StatusCode::TOO_MANY_PARAMETERS, + err_msg, + func_handle_index, + module, + ); + } + + let parameter_allowed = + check_gas_charge_post_function(&view, func_signature, return_signature); + + if !parameter_allowed { + let full_path_module_name = generate_full_module_name(func_handle_index, view); + + let err_msg = format!( + "function {:?} in module {:?} has incorrect parameter type or return type.", + gas_validate_function, full_path_module_name + ); + return generate_vm_error( + StatusCode::TYPE_MISMATCH, + err_msg, + func_handle_index, + module, + ); + } + } + } + } + + Ok(true) +} + +fn generate_full_module_name( + fhandle_index: FunctionHandleIndex, + view: BinaryIndexedView, +) -> String { + let fhandle = view.function_handle_at(fhandle_index); + let module_handle = view.module_handle_at(fhandle.module); + + let module_address = view + .address_identifier_at(module_handle.address) + .to_hex_literal(); + let module_name = view.identifier_at(module_handle.name); + format!("{}::{}", module_address, module_name) +} + +pub fn generate_vm_error( + status_code: StatusCode, + error_msg: String, + fhandle: FunctionHandleIndex, + module: &CompiledModule, +) -> VMResult { + Err(PartialVMError::new(status_code) + .with_message(error_msg) + .at_code_offset(FunctionDefinitionIndex::new(fhandle.0), 0_u16) + .finish(Location::Module(module.self_id()))) +} + +fn check_if_function_exist( + module: &CompiledModule, + function_name: &String, +) -> (bool, FunctionHandleIndex) { + let module_bin_view = BinaryIndexedView::Module(module); + for fdef in module.function_defs.iter() { + let func_handle = module_bin_view.function_handle_at(fdef.function); + let module_address = module.address().to_hex_literal(); + let module_name = module.name().to_string(); + let func_name = module_bin_view.identifier_at(func_handle.name).to_string(); + let full_func_name = format!("{}::{}::{}", module_address, module_name, func_name); + if &full_func_name == function_name { + let fhandle_index = fdef.function.0; + return (true, FunctionHandleIndex::new(fhandle_index)); + } + } + + (false, FunctionHandleIndex::new(0)) +} + +fn check_gas_validate_function( + view: &BinaryIndexedView, + func_signature: &Signature, + return_signature: &Signature, +) -> bool { + // let mut parameter_check_result = false; + let first_parameter = func_signature.0.get(0).unwrap(); + + let check_struct_type = |struct_handle_idx: &StructHandleIndex| -> bool { + let struct_name = struct_full_name_from_sid(struct_handle_idx, view); + if struct_name == "0x2::context::Context" { + return true; + } + + false + }; + + let parameter_check_result = match first_parameter { + SignatureToken::Reference(reference) => match reference.as_ref() { + SignatureToken::Struct(struct_handle_idx) => check_struct_type(struct_handle_idx), + _ => false, + }, + SignatureToken::Struct(struct_handle_idx) => check_struct_type(struct_handle_idx), + _ => false, + }; + + if !parameter_check_result { + return parameter_check_result; + } + + if return_signature.len() != 1 { + return false; + } + + let first_return_signature = return_signature.0.get(0).unwrap(); + matches!(first_return_signature, SignatureToken::Bool) +} + +fn check_gas_charge_post_function( + view: &BinaryIndexedView, + func_signature: &Signature, + return_signature: &Signature, +) -> bool { + let first_parameter = func_signature.0.get(0).unwrap(); + + let check_struct_type = |struct_handle_idx: &StructHandleIndex| -> bool { + let struct_name = struct_full_name_from_sid(struct_handle_idx, view); + if struct_name == "0x2::context::Context" { + return true; + } + + false + }; + + let first_checking_result = { + match first_parameter { + SignatureToken::MutableReference(reference) => match reference.as_ref() { + SignatureToken::Struct(struct_handle_idx) => check_struct_type(struct_handle_idx), + _ => false, + }, + SignatureToken::Struct(struct_handle_idx) => check_struct_type(struct_handle_idx), + _ => false, + } + }; + + if !first_checking_result { + return first_checking_result; + } + + let second_parameter = func_signature.0.get(1).unwrap(); + let second_checking_result = matches!(second_parameter, SignatureToken::U128); + + if !second_checking_result { + return second_checking_result; + } + + if return_signature.len() != 1 { + return false; + } + + let first_return_signature = return_signature.0.get(0).unwrap(); + matches!(first_return_signature, SignatureToken::Bool) +} + // Find the module where a function is located based on its InstantiationIndex. fn load_compiled_module_from_finst_idx( db: &Resolver, diff --git a/moveos/moveos/src/moveos.rs b/moveos/moveos/src/moveos.rs index 7942eaa5db..5f8dda4d7d 100644 --- a/moveos/moveos/src/moveos.rs +++ b/moveos/moveos/src/moveos.rs @@ -5,8 +5,10 @@ use crate::gas::table::{initial_cost_schedule, MoveOSGasMeter}; use crate::vm::moveos_vm::{MoveOSSession, MoveOSVM}; use anyhow::{bail, ensure, Result}; use backtrace::Backtrace; +use itertools::Itertools; use move_binary_format::errors::VMError; use move_binary_format::errors::{vm_status_of_result, Location, PartialVMError, VMResult}; +use move_core_types::value::MoveValue; use move_core_types::vm_status::{KeptVMStatus, VMStatus}; use move_core_types::{ account_address::AccountAddress, identifier::Identifier, vm_status::StatusCode, @@ -20,6 +22,7 @@ use moveos_store::transaction_store::TransactionDBStore; use moveos_store::MoveOSStore; use moveos_types::function_return_value::FunctionResult; use moveos_types::module_binding::MoveFunctionCaller; +use moveos_types::move_types::FunctionId; use moveos_types::moveos_std::tx_context::TxContext; use moveos_types::moveos_std::tx_result::TxResult; use moveos_types::startup_info::StartupInfo; @@ -29,6 +32,7 @@ use moveos_types::transaction::{ MoveOSTransaction, TransactionOutput, VerifiedMoveAction, VerifiedMoveOSTransaction, }; use moveos_types::{h256::H256, transaction::FunctionCall}; +use moveos_verifier::metadata::load_module_metadata; pub struct MoveOSConfig { pub vm_config: VMConfig, @@ -211,11 +215,11 @@ impl MoveOS { match self.execute_user_action( &mut session, - action, + action.clone(), pre_execute_functions.clone(), post_execute_functions.clone(), ) { - Ok(status) => self.execution_cleanup(session, status), + Ok(status) => self.execution_cleanup(session, status, Some(action)), Err((vm_err, need_respawn)) => { if need_respawn { let mut s = session.respawn(system_env); @@ -231,14 +235,165 @@ impl MoveOS { // We just cleanup with the VM error return by `execute_user_action`, ignore // the result of `execute_pre_and_post` // TODO: do we need to handle the result of `execute_pre_and_post` after respawn? - self.execution_cleanup(s, vm_err.into_vm_status()) + self.execution_cleanup(s, vm_err.into_vm_status(), None) } else { - self.execution_cleanup(session, vm_err.into_vm_status()) + self.execution_cleanup(session, vm_err.into_vm_status(), None) } } } } + fn execute_gas_validate( + &self, + session: &mut MoveOSSession<'_, '_, MoveOSResolverProxy, MoveOSGasMeter>, + action: &VerifiedMoveAction, + ) -> VMResult> { + match action { + VerifiedMoveAction::Function { call } => { + let module_id = &call.function_id.module_id; + let loaded_module_bytes = session.get_data_store().load_module(module_id); + + let module_metadata = load_module_metadata(module_id, loaded_module_bytes)?; + let gas_free_function_info = { + match module_metadata { + None => None, + Some(runtime_metadata) => Some(runtime_metadata.gas_free_function_map), + } + }; + + let called_function_name = call.function_id.function_name.to_string(); + match gas_free_function_info { + None => Ok(None), + Some(gas_func_info) => { + let full_called_function = format!( + "0x{}::{}::{}", + call.function_id.module_id.address().to_hex(), + call.function_id.module_id.name(), + called_function_name + ); + let gas_func_info_opt = gas_func_info.get(&full_called_function); + + if let Some(gas_func_info) = gas_func_info_opt { + let gas_validate_func_name = gas_func_info.gas_validate.clone(); + + let split_function = gas_validate_func_name.split("::").collect_vec(); + if split_function.len() != 3 { + return Err(PartialVMError::new(StatusCode::VM_EXTENSION_ERROR) + .with_message( + "The name of the gas_validate_function is incorrect." + .to_string(), + ) + .finish(Location::Module(call.clone().function_id.module_id))); + } + let real_gas_validate_func_name = + split_function.get(2).unwrap().to_string(); + + let gas_validate_func_call = FunctionCall::new( + FunctionId::new( + call.function_id.module_id.clone(), + Identifier::new(real_gas_validate_func_name).unwrap(), + ), + vec![], + vec![], + ); + + let return_value = session + .execute_function_bypass_visibility(gas_validate_func_call)?; + + return if !return_value.is_empty() { + let first_return_value = return_value.get(0).unwrap(); + Ok(Some( + bcs::from_bytes::(first_return_value.value.as_slice()) + .expect( + "the return value of gas validate function should be bool", + ), + )) + } else { + return Err(PartialVMError::new(StatusCode::VM_EXTENSION_ERROR) + .with_message( + "the return value of gas_validate_function is empty." + .to_string(), + ) + .finish(Location::Module(call.clone().function_id.module_id))); + }; + } + + Ok(None) + } + } + } + _ => Ok(None), + } + } + + fn execute_gas_charge_post( + &self, + session: &mut MoveOSSession<'_, '_, MoveOSResolverProxy, MoveOSGasMeter>, + action: &VerifiedMoveAction, + ) -> VMResult> { + match action { + VerifiedMoveAction::Function { call } => { + let module_id = &call.function_id.module_id; + let loaded_module_bytes = session.get_data_store().load_module(module_id); + + let module_metadata = load_module_metadata(module_id, loaded_module_bytes)?; + let gas_free_function_info = { + match module_metadata { + None => None, + Some(runtime_metadata) => Some(runtime_metadata.gas_free_function_map), + } + }; + + let called_function_name = call.function_id.function_name.to_string(); + match gas_free_function_info { + None => Ok(None), + Some(gas_func_info) => { + let full_called_function = format!( + "0x{}::{}::{}", + call.function_id.module_id.address().to_hex(), + call.function_id.module_id.name(), + called_function_name + ); + let gas_func_info_opt = gas_func_info.get(&full_called_function); + + if let Some(gas_func_info) = gas_func_info_opt { + let gas_charge_post_func_name = gas_func_info.gas_charge_post.clone(); + + let split_function = + gas_charge_post_func_name.split("::").collect_vec(); + if split_function.len() != 3 { + return Err(PartialVMError::new(StatusCode::VM_EXTENSION_ERROR) + .with_message( + "The name of the gas_validate_function is incorrect." + .to_string(), + ) + .finish(Location::Module(call.clone().function_id.module_id))); + } + let real_gas_charge_post_func_name = + split_function.get(2).unwrap().to_string(); + + let gas_validate_func_call = FunctionCall::new( + FunctionId::new( + call.function_id.module_id.clone(), + Identifier::new(real_gas_charge_post_func_name).unwrap(), + ), + vec![], + vec![MoveValue::U128(session.query_gas_used() as u128) + .simple_serialize() + .unwrap()], + ); + + session.execute_function_bypass_visibility(gas_validate_func_call)?; + } + + Ok(None) + } + } + } + _ => Ok(None), + } + } + pub fn execute_and_apply( &mut self, tx: VerifiedMoveOSTransaction, @@ -380,6 +535,7 @@ impl MoveOS { &self, mut session: MoveOSSession<'_, '_, MoveOSResolverProxy, MoveOSGasMeter>, status: VMStatus, + action_opt: Option, ) -> Result { let kept_status = match status.keep_or_discard() { Ok(kept_status) => kept_status, @@ -389,10 +545,35 @@ impl MoveOS { panic!("Discard status: {:?}\n{:?}", discard_status, backtrace); } }; + + let mut can_pay_gas = None; + let action_opt_cloned = action_opt.clone(); + if let Some(action) = action_opt { + can_pay_gas = self.execute_gas_validate(&mut session, &action)?; + } + + let mut gas_payment_account = session.storage_context_mut().tx_context.sender; + if let Some(pay_gas) = can_pay_gas { + if pay_gas { + let verified_action = &action_opt_cloned.clone().unwrap(); + let func_call_opt = { + match verified_action { + VerifiedMoveAction::Function { call } => Some(call), + _ => None, + } + }; + + if let Some(func_call) = func_call_opt { + let module_address = func_call.function_id.module_id.address(); + gas_payment_account = *module_address; + } + } + } + // update txn result to TxContext let gas_used = session.query_gas_used(); //TODO is it a good approach to add tx_result to TxContext? - let tx_result = TxResult::new(&kept_status, gas_used); + let tx_result = TxResult::new(&kept_status, gas_used, gas_payment_account); session .storage_context_mut() .tx_context @@ -405,6 +586,12 @@ impl MoveOS { .execute_function_call(self.system_post_execute_functions.clone(), false) .expect("system_post_execute should not fail."); + if let Some(pay_gas) = can_pay_gas { + if pay_gas { + self.execute_gas_charge_post(&mut session, &action_opt_cloned.unwrap())?; + } + } + let (_ctx, output) = session.finish_with_extensions(kept_status)?; Ok(output) }