diff --git a/aptos-move/aptos-vm/src/aptos_vm.rs b/aptos-move/aptos-vm/src/aptos_vm.rs index 5fb18ab86eb94..c7ced543850b4 100644 --- a/aptos-move/aptos-vm/src/aptos_vm.rs +++ b/aptos-move/aptos-vm/src/aptos_vm.rs @@ -33,7 +33,7 @@ use aptos_framework::{ }; use aptos_gas_algebra::{Gas, GasQuantity, NumBytes, Octa}; use aptos_gas_meter::{AptosGasMeter, GasAlgebra, StandardGasAlgebra, StandardGasMeter}; -use aptos_gas_schedule::{AptosGasParameters, VMGasParameters}; +use aptos_gas_schedule::{AptosGasParameters, TransactionGasParameters, VMGasParameters}; use aptos_logger::{enabled, prelude::*, Level}; use aptos_memory_usage_tracker::MemoryTrackedGasMeter; use aptos_metrics_core::TimerHelper; @@ -53,6 +53,7 @@ use aptos_types::{ move_utils::as_move_value::AsMoveValue, on_chain_config::{ new_epoch_event_key, ConfigurationResource, FeatureFlag, Features, OnChainConfig, + OnChainConsensusConfig, OnChainRandomnessConfig, RandomnessConfigMoveStruct, TimedFeatureOverride, TimedFeatures, TimedFeaturesBuilder, }, randomness::Randomness, @@ -200,6 +201,7 @@ pub struct AptosVM { gas_params: Result, pub(crate) storage_gas_params: Result, timed_features: TimedFeatures, + randomness_enabled: bool, } impl AptosVM { @@ -239,6 +241,12 @@ impl AptosVM { let aggregator_v2_type_tagging = override_is_delayed_field_optimization_capable && features.is_aggregator_v2_delayed_fields_enabled(); + let consensus_config = OnChainConsensusConfig::fetch_config(resolver).unwrap_or_default(); + let randomness_config = RandomnessConfigMoveStruct::fetch_config(resolver) + .and_then(|x| OnChainRandomnessConfig::try_from(x).ok()) + .unwrap_or_else(OnChainRandomnessConfig::default_if_missing); + let randomness_enabled = + consensus_config.is_vtxn_enabled() && randomness_config.randomness_enabled(); let move_vm = MoveVmExt::new( native_gas_params, misc_gas_params, @@ -258,6 +266,7 @@ impl AptosVM { gas_params, storage_gas_params, timed_features, + randomness_enabled, } } @@ -735,12 +744,12 @@ impl AptosVM { fn validate_and_execute_entry_function( &self, - resolver: &impl AptosMoveResolver, session: &mut SessionExt, gas_meter: &mut impl AptosGasMeter, traversal_context: &mut TraversalContext, senders: Vec, entry_fn: &EntryFunction, + txn_data: &TransactionMetadata, ) -> Result<(), VMStatus> { // Note: Feature gating is needed here because the traversal of the dependencies could // result in shallow-loading of the modules and therefore subtle changes in @@ -761,7 +770,7 @@ impl AptosVM { entry_fn.ty_args(), )?; - if is_friend_or_private && has_randomness_attribute(resolver, session, entry_fn)? { + if is_friend_or_private && txn_data.required_deposit.is_some() { let txn_context = session .get_native_extensions() .get_mut::(); @@ -824,12 +833,12 @@ impl AptosVM { TransactionPayload::EntryFunction(entry_fn) => { session.execute(|session| { self.validate_and_execute_entry_function( - resolver, session, gas_meter, traversal_context, txn_data.senders(), entry_fn, + txn_data, ) })?; }, @@ -930,13 +939,13 @@ impl AptosVM { aptos_try!({ return_on_failure!(session.execute(|session| self .execute_multisig_entry_function( - resolver, session, gas_meter, traversal_context, payload.multisig_address, entry_function, new_published_modules_loaded, + txn_data, ))); // TODO: Deduplicate this against execute_multisig_transaction // A bit tricky since we need to skip success/failure cleanups, @@ -1052,13 +1061,13 @@ impl AptosVM { MultisigTransactionPayload::EntryFunction(entry_function) => { session.execute(|session| { self.execute_multisig_entry_function( - resolver, session, gas_meter, traversal_context, txn_payload.multisig_address, &entry_function, new_published_modules_loaded, + txn_data, ) }) }, @@ -1153,23 +1162,23 @@ impl AptosVM { fn execute_multisig_entry_function( &self, - resolver: &impl AptosMoveResolver, session: &mut SessionExt, gas_meter: &mut impl AptosGasMeter, traversal_context: &mut TraversalContext, multisig_address: AccountAddress, payload: &EntryFunction, new_published_modules_loaded: &mut bool, + txn_data: &TransactionMetadata, ) -> Result<(), VMStatus> { // If txn args are not valid, we'd still consider the transaction as executed but // failed. This is primarily because it's unrecoverable at this point. self.validate_and_execute_entry_function( - resolver, session, gas_meter, traversal_context, vec![multisig_address], payload, + txn_data, )?; // Resolve any pending module publishes in case the multisig transaction is deploying @@ -1616,20 +1625,22 @@ impl AptosVM { gas_meter: &mut impl AptosGasMeter, traversal_context: &mut TraversalContext<'a>, ) -> (VMStatus, VMOutput) { - let txn_data = TransactionMetadata::new(txn); + let mut txn_data = TransactionMetadata::new(txn); // Revalidate the transaction. let mut prologue_session = unwrap_or_discard!(PrologueSession::new(self, &txn_data, resolver)); - unwrap_or_discard!( - prologue_session.execute(|session| self.validate_signed_transaction( + unwrap_or_discard!(prologue_session.execute(|session| { + let required_deposit = self.get_required_deposit( session, resolver, - txn, + &gas_meter.vm_gas_params().txn, &txn_data, - log_context - )) - ); + txn.payload(), + ); + txn_data.set_required_deposit(required_deposit); + self.validate_signed_transaction(session, resolver, txn, &txn_data, log_context) + })); let storage_gas_params = unwrap_or_discard!(get_or_vm_startup_failure( &self.storage_gas_params, @@ -2300,6 +2311,41 @@ impl AptosVM { }, }) } + + pub fn get_required_deposit( + &self, + session: &mut SessionExt, + resolver: &impl AptosMoveResolver, + txn_gas_params: &TransactionGasParameters, + txn_metadata: &TransactionMetadata, + payload: &TransactionPayload, + ) -> Option { + match payload { + TransactionPayload::EntryFunction(entry_func) => { + if self.randomness_enabled + && has_randomness_attribute(resolver, session, entry_func).unwrap_or(false) + { + let max_execution_gas: Gas = txn_gas_params + .max_execution_gas + .to_unit_round_up_with_params(txn_gas_params); + let max_io_gas: Gas = txn_gas_params + .max_io_gas + .to_unit_round_up_with_params(txn_gas_params); + let cand_0 = txn_metadata.gas_unit_price * (max_execution_gas + max_io_gas) + + txn_gas_params.max_storage_fee; + let cand_1 = + txn_metadata.gas_unit_price * txn_gas_params.maximum_number_of_gas_units; + let required_fee_deposit = min(cand_0, cand_1); + Some(u64::from(required_fee_deposit)) + } else { + None + } + }, + TransactionPayload::Script(_) + | TransactionPayload::ModuleBundle(_) + | TransactionPayload::Multisig(_) => None, + } + } } // Executor external API @@ -2431,10 +2477,23 @@ impl VMValidator for AptosVM { return VMValidatorResult::error(StatusCode::INVALID_SIGNATURE); }, }; - let txn_data = TransactionMetadata::new(&txn); + let mut txn_data = TransactionMetadata::new(&txn); let resolver = self.as_move_resolver(&state_view); let mut session = self.new_session(&resolver, SessionId::prologue_meta(&txn_data)); + let required_deposit = if let Ok(gas_params) = &self.gas_params { + self.get_required_deposit( + &mut session, + &resolver, + &gas_params.vm.txn, + &txn_data, + txn.payload(), + ) + } else { + return VMValidatorResult::error(StatusCode::GAS_PARAMS_MISSING); + }; + + txn_data.set_required_deposit(required_deposit); // Increment the counter for transactions verified. let (counter_label, result) = match self.validate_signed_transaction( diff --git a/aptos-move/aptos-vm/src/errors.rs b/aptos-move/aptos-vm/src/errors.rs index b14c4e87e34d7..0b95668fb4ef5 100644 --- a/aptos-move/aptos-vm/src/errors.rs +++ b/aptos-move/aptos-vm/src/errors.rs @@ -35,6 +35,8 @@ pub const ESEQUENCE_NUMBER_TOO_BIG: u64 = 1008; pub const ESECONDARY_KEYS_ADDRESSES_COUNT_MISMATCH: u64 = 1009; // Gas payer account missing in gas payer tx pub const EGAS_PAYER_ACCOUNT_MISSING: u64 = 1010; +// Insufficient balance to cover the required deposit. +pub const EINSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT: u64 = 1011; // Specified account is not a multisig account. const EACCOUNT_NOT_MULTISIG: u64 = 2002; @@ -124,6 +126,9 @@ pub fn convert_prologue_error( (INVALID_ARGUMENT, EGAS_PAYER_ACCOUNT_MISSING) => { StatusCode::GAS_PAYER_ACCOUNT_MISSING }, + (INVALID_STATE, EINSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT) => { + StatusCode::INSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT + }, (category, reason) => { let err_msg = format!("[aptos_vm] Unexpected prologue Move abort: {:?}::{:?} (Category: {:?} Reason: {:?})", location, code, category, reason); diff --git a/aptos-move/aptos-vm/src/move_vm_ext/session/user_transaction_sessions/prologue.rs b/aptos-move/aptos-vm/src/move_vm_ext/session/user_transaction_sessions/prologue.rs index a2f4660544433..673466368a3e2 100644 --- a/aptos-move/aptos-vm/src/move_vm_ext/session/user_transaction_sessions/prologue.rs +++ b/aptos-move/aptos-vm/src/move_vm_ext/session/user_transaction_sessions/prologue.rs @@ -23,9 +23,9 @@ pub struct PrologueSession<'r, 'l> { } impl<'r, 'l> PrologueSession<'r, 'l> { - pub fn new( + pub fn new<'m>( vm: &'l AptosVM, - txn_meta: &'l TransactionMetadata, + txn_meta: &'m TransactionMetadata, resolver: &'r impl AptosMoveResolver, ) -> Result { let session_id = SessionId::prologue_meta(txn_meta); diff --git a/aptos-move/aptos-vm/src/transaction_metadata.rs b/aptos-move/aptos-vm/src/transaction_metadata.rs index cdc83dba3f648..e6533b74607a1 100644 --- a/aptos-move/aptos-vm/src/transaction_metadata.rs +++ b/aptos-move/aptos-vm/src/transaction_metadata.rs @@ -25,6 +25,7 @@ pub struct TransactionMetadata { pub chain_id: ChainId, pub script_hash: Vec, pub script_size: NumBytes, + pub required_deposit: Option, } impl TransactionMetadata { @@ -63,6 +64,7 @@ impl TransactionMetadata { TransactionPayload::Script(s) => (s.code().len() as u64).into(), _ => NumBytes::zero(), }, + required_deposit: None, } } @@ -119,4 +121,8 @@ impl TransactionMetadata { pub fn is_multi_agent(&self) -> bool { !self.secondary_signers.is_empty() || self.fee_payer.is_some() } + + pub fn set_required_deposit(&mut self, required_deposit: Option) { + self.required_deposit = required_deposit; + } } diff --git a/aptos-move/aptos-vm/src/transaction_validation.rs b/aptos-move/aptos-vm/src/transaction_validation.rs index cb4ca1a0a558c..24bc87f758698 100644 --- a/aptos-move/aptos-vm/src/transaction_validation.rs +++ b/aptos-move/aptos-vm/src/transaction_validation.rs @@ -14,7 +14,7 @@ use crate::{ use aptos_gas_algebra::Gas; use aptos_types::{ account_config::constants::CORE_CODE_ADDRESS, fee_statement::FeeStatement, - on_chain_config::Features, transaction::Multisig, + move_utils::as_move_value::AsMoveValue, on_chain_config::Features, transaction::Multisig, }; use aptos_vm_logging::log_schema::AdapterLogSchema; use fail::fail_point; @@ -36,10 +36,21 @@ pub static APTOS_TRANSACTION_VALIDATION: Lazy = module_addr: CORE_CODE_ADDRESS, module_name: Identifier::new("transaction_validation").unwrap(), fee_payer_prologue_name: Identifier::new("fee_payer_script_prologue").unwrap(), + fee_payer_prologue_collect_deposit_name: Identifier::new( + "fee_payer_script_prologue_collect_deposit", + ) + .unwrap(), script_prologue_name: Identifier::new("script_prologue").unwrap(), + script_prologue_collect_deposit_name: Identifier::new("script_prologue_collect_deposit") + .unwrap(), multi_agent_prologue_name: Identifier::new("multi_agent_script_prologue").unwrap(), user_epilogue_name: Identifier::new("epilogue").unwrap(), + user_epilogue_return_deposit_name: Identifier::new("epilogue_return_deposit").unwrap(), user_epilogue_gas_payer_name: Identifier::new("epilogue_gas_payer").unwrap(), + user_epilogue_gas_payer_return_deposit_name: Identifier::new( + "epilogue_gas_payer_return_deposit", + ) + .unwrap(), }); /// On-chain functions used to validate transactions @@ -48,10 +59,14 @@ pub struct TransactionValidation { pub module_addr: AccountAddress, pub module_name: Identifier, pub fee_payer_prologue_name: Identifier, + pub fee_payer_prologue_collect_deposit_name: Identifier, pub script_prologue_name: Identifier, + pub script_prologue_collect_deposit_name: Identifier, pub multi_agent_prologue_name: Identifier, pub user_epilogue_name: Identifier, + pub user_epilogue_return_deposit_name: Identifier, pub user_epilogue_gas_payer_name: Identifier, + pub user_epilogue_gas_payer_return_deposit_name: Identifier, } impl TransactionValidation { @@ -86,12 +101,11 @@ pub(crate) fn run_script_prologue( .iter() .map(|auth_key| MoveValue::vector_u8(auth_key.to_vec())) .collect(); - let (prologue_function_name, args) = if let (Some(fee_payer), Some(fee_payer_auth_key)) = ( txn_data.fee_payer(), txn_data.fee_payer_authentication_key.as_ref(), ) { - let args = vec![ + let mut args = vec![ MoveValue::Signer(txn_data.sender), MoveValue::U64(txn_sequence_number), MoveValue::vector_u8(txn_authentication_key), @@ -104,7 +118,15 @@ pub(crate) fn run_script_prologue( MoveValue::U64(txn_expiration_timestamp_secs), MoveValue::U8(chain_id.id()), ]; - (&APTOS_TRANSACTION_VALIDATION.fee_payer_prologue_name, args) + if txn_data.required_deposit.is_some() { + args.push(txn_data.required_deposit.as_move_value()); + ( + &APTOS_TRANSACTION_VALIDATION.fee_payer_prologue_collect_deposit_name, + args, + ) + } else { + (&APTOS_TRANSACTION_VALIDATION.fee_payer_prologue_name, args) + } } else if txn_data.is_multi_agent() { let args = vec![ MoveValue::Signer(txn_data.sender), @@ -122,7 +144,7 @@ pub(crate) fn run_script_prologue( args, ) } else { - let args = vec![ + let mut args = vec![ MoveValue::Signer(txn_data.sender), MoveValue::U64(txn_sequence_number), MoveValue::vector_u8(txn_authentication_key), @@ -132,7 +154,15 @@ pub(crate) fn run_script_prologue( MoveValue::U8(chain_id.id()), MoveValue::vector_u8(txn_data.script_hash.clone()), ]; - (&APTOS_TRANSACTION_VALIDATION.script_prologue_name, args) + if txn_data.required_deposit.is_some() { + args.push(txn_data.required_deposit.as_move_value()); + ( + &APTOS_TRANSACTION_VALIDATION.script_prologue_collect_deposit_name, + args, + ) + } else { + (&APTOS_TRANSACTION_VALIDATION.script_prologue_name, args) + } }; session .execute_function_bypass_visibility( @@ -196,33 +226,60 @@ fn run_epilogue( // We can unconditionally do this as this condition can only be true if the prologue // accepted it, in which case the gas payer feature is enabled. if let Some(fee_payer) = txn_data.fee_payer() { - session.execute_function_bypass_visibility( - &APTOS_TRANSACTION_VALIDATION.module_id(), - &APTOS_TRANSACTION_VALIDATION.user_epilogue_gas_payer_name, - vec![], - serialize_values(&vec![ + let (func_name, args) = { + let mut args = vec![ MoveValue::Signer(txn_data.sender), MoveValue::Address(fee_payer), MoveValue::U64(fee_statement.storage_fee_refund()), MoveValue::U64(txn_gas_price.into()), MoveValue::U64(txn_max_gas_units.into()), MoveValue::U64(gas_remaining.into()), - ]), + ]; + if txn_data.required_deposit.is_some() { + args.push(txn_data.required_deposit.as_move_value()); + ( + &APTOS_TRANSACTION_VALIDATION.user_epilogue_gas_payer_return_deposit_name, + args, + ) + } else { + ( + &APTOS_TRANSACTION_VALIDATION.user_epilogue_gas_payer_name, + args, + ) + } + }; + session.execute_function_bypass_visibility( + &APTOS_TRANSACTION_VALIDATION.module_id(), + func_name, + vec![], + serialize_values(&args), &mut UnmeteredGasMeter, ) } else { // Regular tx, run the normal epilogue - session.execute_function_bypass_visibility( - &APTOS_TRANSACTION_VALIDATION.module_id(), - &APTOS_TRANSACTION_VALIDATION.user_epilogue_name, - vec![], - serialize_values(&vec![ + let (func_name, args) = { + let mut args = vec![ MoveValue::Signer(txn_data.sender), MoveValue::U64(fee_statement.storage_fee_refund()), MoveValue::U64(txn_gas_price.into()), MoveValue::U64(txn_max_gas_units.into()), MoveValue::U64(gas_remaining.into()), - ]), + ]; + if txn_data.required_deposit.is_some() { + args.push(txn_data.required_deposit.as_move_value()); + ( + &APTOS_TRANSACTION_VALIDATION.user_epilogue_return_deposit_name, + args, + ) + } else { + (&APTOS_TRANSACTION_VALIDATION.user_epilogue_name, args) + } + }; + session.execute_function_bypass_visibility( + &APTOS_TRANSACTION_VALIDATION.module_id(), + func_name, + vec![], + serialize_values(&args), &mut UnmeteredGasMeter, ) } diff --git a/aptos-move/e2e-move-tests/src/harness.rs b/aptos-move/e2e-move-tests/src/harness.rs index ec89d0074cfdb..11751def4360a 100644 --- a/aptos-move/e2e-move-tests/src/harness.rs +++ b/aptos-move/e2e-move-tests/src/harness.rs @@ -76,7 +76,7 @@ pub struct MoveHarness { /// The last counted transaction sequence number, by account address. txn_seq_no: BTreeMap, - default_gas_unit_price: u64, + pub default_gas_unit_price: u64, max_gas_per_txn: u64, } diff --git a/aptos-move/e2e-move-tests/src/tests/randomness.data/pack/sources/test.move b/aptos-move/e2e-move-tests/src/tests/randomness.data/pack/sources/test.move index fcca05f37e222..463f9b000fc37 100644 --- a/aptos-move/e2e-move-tests/src/tests/randomness.data/pack/sources/test.move +++ b/aptos-move/e2e-move-tests/src/tests/randomness.data/pack/sources/test.move @@ -1,5 +1,7 @@ module 0x1::test { + use aptos_framework::aptos_coin::AptosCoin; use aptos_framework::randomness; + use aptos_framework::coin; entry fun ok_if_not_annotated_and_not_using_randomness() { // Do nothing. @@ -18,4 +20,13 @@ module 0x1::test { entry fun fail_if_not_annotated_and_using_randomness() { let _ = randomness::u64_integer(); } + + #[randomness] + /// Transfer some amount out to 2 recipients with a random split. + entry fun transfer_lucky_money(sender: &signer, amount: u64, recipient_0: address, recipient_1: address) { + let part_0 = randomness::u64_range(0, amount + 1); + let part_1 = amount - part_0; + coin::transfer(sender, recipient_0, part_0); + coin::transfer(sender, recipient_1, part_1); + } } diff --git a/aptos-move/e2e-move-tests/src/tests/randomness_test_and_abort.rs b/aptos-move/e2e-move-tests/src/tests/randomness_test_and_abort.rs index 0cb7eb3c7d285..0dbb2a2a2afe0 100644 --- a/aptos-move/e2e-move-tests/src/tests/randomness_test_and_abort.rs +++ b/aptos-move/e2e-move-tests/src/tests/randomness_test_and_abort.rs @@ -6,12 +6,18 @@ use aptos_framework::BuiltPackage; use aptos_language_e2e_tests::account::{Account, TransactionBuilder}; use aptos_types::{ account_address::AccountAddress, + move_utils::{as_move_value::AsMoveValue, MemberId}, on_chain_config::OnChainConfig, randomness::PerBlockRandomness, transaction::{ExecutionStatus, Script, TransactionStatus}, }; -use claims::assert_ok; -use move_core_types::{ident_str, language_storage::ModuleId, vm_status::AbortLocation}; +use claims::{assert_gt, assert_lt, assert_ok}; +use move_core_types::{ + ident_str, + language_storage::ModuleId, + value::{serialize_values, MoveValue}, + vm_status::AbortLocation, +}; // Error codes from randomness module. const E_API_USE_SUSCEPTIBLE_TO_TEST_AND_ABORT: u64 = 1; @@ -86,6 +92,7 @@ fn test_unbiasable_annotation() { "0x1::test::ok_if_annotated_and_not_using_randomness", "0x1::test::ok_if_annotated_and_using_randomness", ]; + for entry_func in should_succeed { let status = run_entry_func(&mut h, "0xa11ce", entry_func); assert_success!(status); @@ -115,6 +122,76 @@ fn test_unbiasable_annotation() { } } +#[test] +fn test_undergas_attack_prevention() { + let mut h = MoveHarness::new(); + deploy_code(AccountAddress::ONE, "randomness.data/pack", &mut h) + .expect("building package must succeed"); + set_randomness_seed(&mut h); + + // Modify gas parameters so the required deposit for randomness txns is 234_000_000 when gas price is 1. + h.modify_gas_schedule(|gas_params| { + gas_params.vm.txn.max_execution_gas = 200_000_000.into(); + gas_params.vm.txn.max_io_gas = 30_000_000.into(); + gas_params.vm.txn.max_storage_fee = 4_000_000.into(); + gas_params.vm.txn.maximum_number_of_gas_units = 234_000_001.into(); + }); + + h.set_default_gas_unit_price(1); + + // A function to send some amount to 2 people where how to split between the 2 is randomized. + let func: MemberId = str::parse("0x1::test::transfer_lucky_money").unwrap(); + let recipient_0 = h.new_account_with_balance_and_sequence_number(0, 11); + let recipient_1 = h.new_account_with_balance_and_sequence_number(0, 12); + + // A txn should be discarded if the sender balance is not enough to pay required deposit. + let sender = h.new_account_with_balance_and_sequence_number(999, 123); + let args = vec![ + 1_000_000_000_u64.as_move_value(), + MoveValue::Address(*recipient_0.address()), + MoveValue::Address(*recipient_1.address()), + ]; + let status = h.run_entry_function(&sender, func.clone(), vec![], serialize_values(&args)); + assert!(status.is_discarded()); + assert_eq!(999, h.read_aptos_balance(sender.address())); + assert_eq!(0, h.read_aptos_balance(recipient_0.address())); + assert_eq!(0, h.read_aptos_balance(recipient_1.address())); + + // A txn should abort but be kept if the sender doesn't have enough balance to complete the transfer. + let sender = h.new_account_with_balance_and_sequence_number(234_999_999, 456); + let args = vec![ + 1_000_000_000_u64.as_move_value(), + MoveValue::Address(*recipient_0.address()), + MoveValue::Address(*recipient_1.address()), + ]; + let status = h.run_entry_function(&sender, func.clone(), vec![], serialize_values(&args)); + let status = assert_ok!(status.as_kept_status()); + assert!(matches!(status, ExecutionStatus::MoveAbort { .. })); + let sender_balance = h.read_aptos_balance(sender.address()); + assert_gt!(sender_balance, 234_000_000); + assert_lt!(sender_balance, 234_999_999); + assert_eq!(0, h.read_aptos_balance(recipient_0.address())); + assert_eq!(0, h.read_aptos_balance(recipient_1.address())); + + // Otherwise, the txn should finish normally. + let sender = h.new_account_with_balance_and_sequence_number(1_234_999_999, 789); + let args = vec![ + 1_000_000_000_u64.as_move_value(), + MoveValue::Address(*recipient_0.address()), + MoveValue::Address(*recipient_1.address()), + ]; + let status = h.run_entry_function(&sender, func.clone(), vec![], serialize_values(&args)); + let status = assert_ok!(status.as_kept_status()); + assert!(matches!(status, ExecutionStatus::Success)); + let sender_balance = h.read_aptos_balance(sender.address()); + assert_gt!(sender_balance, 234_000_000); + assert_lt!(sender_balance, 234_999_999); + assert_eq!( + 1_000_000_000, + h.read_aptos_balance(recipient_0.address()) + h.read_aptos_balance(recipient_1.address()) + ); +} + fn set_randomness_seed(h: &mut MoveHarness) { let fx = h.aptos_framework_account(); let mut pbr = h diff --git a/aptos-move/framework/aptos-framework/doc/transaction_validation.md b/aptos-move/framework/aptos-framework/doc/transaction_validation.md index 96a25e46bbc43..6ba2db0546a03 100644 --- a/aptos-move/framework/aptos-framework/doc/transaction_validation.md +++ b/aptos-move/framework/aptos-framework/doc/transaction_validation.md @@ -8,24 +8,36 @@ - [Resource `TransactionValidation`](#0x1_transaction_validation_TransactionValidation) - [Constants](#@Constants_0) - [Function `initialize`](#0x1_transaction_validation_initialize) +- [Function `collect_deposit`](#0x1_transaction_validation_collect_deposit) +- [Function `return_deposit`](#0x1_transaction_validation_return_deposit) - [Function `prologue_common`](#0x1_transaction_validation_prologue_common) - [Function `script_prologue`](#0x1_transaction_validation_script_prologue) +- [Function `script_prologue_collect_deposit`](#0x1_transaction_validation_script_prologue_collect_deposit) - [Function `multi_agent_script_prologue`](#0x1_transaction_validation_multi_agent_script_prologue) - [Function `multi_agent_common_prologue`](#0x1_transaction_validation_multi_agent_common_prologue) - [Function `fee_payer_script_prologue`](#0x1_transaction_validation_fee_payer_script_prologue) +- [Function `fee_payer_script_prologue_collect_deposit`](#0x1_transaction_validation_fee_payer_script_prologue_collect_deposit) - [Function `epilogue`](#0x1_transaction_validation_epilogue) +- [Function `epilogue_return_deposit`](#0x1_transaction_validation_epilogue_return_deposit) - [Function `epilogue_gas_payer`](#0x1_transaction_validation_epilogue_gas_payer) +- [Function `epilogue_gas_payer_return_deposit`](#0x1_transaction_validation_epilogue_gas_payer_return_deposit) - [Specification](#@Specification_1) - [High-level Requirements](#high-level-req) - [Module-level Specification](#module-level-spec) - [Function `initialize`](#@Specification_1_initialize) + - [Function `collect_deposit`](#@Specification_1_collect_deposit) + - [Function `return_deposit`](#@Specification_1_return_deposit) - [Function `prologue_common`](#@Specification_1_prologue_common) - [Function `script_prologue`](#@Specification_1_script_prologue) + - [Function `script_prologue_collect_deposit`](#@Specification_1_script_prologue_collect_deposit) - [Function `multi_agent_script_prologue`](#@Specification_1_multi_agent_script_prologue) - [Function `multi_agent_common_prologue`](#@Specification_1_multi_agent_common_prologue) - [Function `fee_payer_script_prologue`](#@Specification_1_fee_payer_script_prologue) + - [Function `fee_payer_script_prologue_collect_deposit`](#@Specification_1_fee_payer_script_prologue_collect_deposit) - [Function `epilogue`](#@Specification_1_epilogue) + - [Function `epilogue_return_deposit`](#@Specification_1_epilogue_return_deposit) - [Function `epilogue_gas_payer`](#@Specification_1_epilogue_gas_payer) + - [Function `epilogue_gas_payer_return_deposit`](#@Specification_1_epilogue_gas_payer_return_deposit)
use 0x1::account;
@@ -35,6 +47,7 @@
 use 0x1::coin;
 use 0x1::error;
 use 0x1::features;
+use 0x1::option;
 use 0x1::signer;
 use 0x1::system_addresses;
 use 0x1::timestamp;
@@ -163,6 +176,15 @@ Transaction exceeded its allocated max gas
 
 
 
+
+
+
+
+
const PROLOGUE_EINSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT: u64 = 1011;
+
+ + + Prologue errors. These are separated out from the other errors in this @@ -260,6 +282,65 @@ Only called during genesis to initialize system resources for this module. + + + + +## Function `collect_deposit` + +Called in prologue to optionally hold some amount for special txns (e.g. randomness txns). +return_deposit() should be invoked in the corresponding epilogue with the same arguments. + + +
fun collect_deposit(gas_payer: address, amount: option::Option<u64>)
+
+ + + +
+Implementation + + +
fun collect_deposit(gas_payer: address, amount: Option<u64>) {
+    if (option::is_some(&amount)) {
+        let amount = option::extract(&mut amount);
+        let balance = coin::balance<AptosCoin>(gas_payer);
+        assert!(balance >= amount, error::invalid_state(PROLOGUE_EINSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT));
+        transaction_fee::burn_fee(gas_payer, amount);
+    }
+}
+
+ + + +
+ + + +## Function `return_deposit` + +Called in epilogue to optionally released the amount held in prologue for special txns (e.g. randomness txns). + + +
fun return_deposit(gas_payer: address, amount: option::Option<u64>)
+
+ + + +
+Implementation + + +
fun return_deposit(gas_payer: address, amount: Option<u64>) {
+    if (option::is_some(&amount)) {
+        let amount = option::extract(&mut amount);
+        transaction_fee::mint_and_refund(gas_payer, amount);
+    }
+}
+
+ + +
@@ -382,6 +463,45 @@ Only called during genesis to initialize system resources for this module. + + + + +## Function `script_prologue_collect_deposit` + +script_prologue() then collect an optional deposit depending on the txn. + +Deposit collection goes last so script_prologue() doesn't have to be aware of the deposit logic. + + +
fun script_prologue_collect_deposit(sender: signer, txn_sequence_number: u64, txn_public_key: vector<u8>, txn_gas_price: u64, txn_max_gas_units: u64, txn_expiration_time: u64, chain_id: u8, script_hash: vector<u8>, required_deposit: option::Option<u64>)
+
+ + + +
+Implementation + + +
fun script_prologue_collect_deposit(
+    sender: signer,
+    txn_sequence_number: u64,
+    txn_public_key: vector<u8>,
+    txn_gas_price: u64,
+    txn_max_gas_units: u64,
+    txn_expiration_time: u64,
+    chain_id: u8,
+    script_hash: vector<u8>,
+    required_deposit: Option<u64>,
+) {
+    let gas_payer = signer::address_of(&sender);
+    script_prologue(sender, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id, script_hash);
+    collect_deposit(gas_payer, required_deposit);
+}
+
+ + +
@@ -531,6 +651,59 @@ Only called during genesis to initialize system resources for this module. + + + + +## Function `fee_payer_script_prologue_collect_deposit` + +fee_payer_script_prologue() then collect an optional deposit depending on the txn. + +Deposit collection goes last so fee_payer_script_prologue() doesn't have to be aware of the deposit logic. + + +
fun fee_payer_script_prologue_collect_deposit(sender: signer, txn_sequence_number: u64, txn_sender_public_key: vector<u8>, secondary_signer_addresses: vector<address>, secondary_signer_public_key_hashes: vector<vector<u8>>, fee_payer_address: address, fee_payer_public_key_hash: vector<u8>, txn_gas_price: u64, txn_max_gas_units: u64, txn_expiration_time: u64, chain_id: u8, required_deposit: option::Option<u64>)
+
+ + + +
+Implementation + + +
fun fee_payer_script_prologue_collect_deposit(
+    sender: signer,
+    txn_sequence_number: u64,
+    txn_sender_public_key: vector<u8>,
+    secondary_signer_addresses: vector<address>,
+    secondary_signer_public_key_hashes: vector<vector<u8>>,
+    fee_payer_address: address,
+    fee_payer_public_key_hash: vector<u8>,
+    txn_gas_price: u64,
+    txn_max_gas_units: u64,
+    txn_expiration_time: u64,
+    chain_id: u8,
+    required_deposit: Option<u64>,
+) {
+    fee_payer_script_prologue(
+        sender,
+        txn_sequence_number,
+        txn_sender_public_key,
+        secondary_signer_addresses,
+        secondary_signer_public_key_hashes,
+        fee_payer_address,
+        fee_payer_public_key_hash,
+        txn_gas_price,
+        txn_max_gas_units,
+        txn_expiration_time,
+        chain_id,
+    );
+    collect_deposit(fee_payer_address, required_deposit);
+}
+
+ + +
@@ -564,6 +737,48 @@ Called by the Adapter + + + + +## Function `epilogue_return_deposit` + +Return the deposit held in prologue, then epilogue(). + +Deposit return goes first so epilogue() doesn't have to be aware of this change. + + +
fun epilogue_return_deposit(account: signer, storage_fee_refunded: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64, required_deposit: option::Option<u64>)
+
+ + + +
+Implementation + + +
fun epilogue_return_deposit(
+    account: signer,
+    storage_fee_refunded: u64,
+    txn_gas_price: u64,
+    txn_max_gas_units: u64,
+    gas_units_remaining: u64,
+    required_deposit: Option<u64>,
+) {
+    let gas_payer = signer::address_of(&account);
+    return_deposit(gas_payer, required_deposit);
+    epilogue(
+        account,
+        storage_fee_refunded,
+        txn_gas_price,
+        txn_max_gas_units,
+        gas_units_remaining,
+    );
+}
+
+ + +
@@ -637,6 +852,49 @@ Called by the Adapter + + + + +## Function `epilogue_gas_payer_return_deposit` + +Return the deposit held in prologue to the gas payer, then epilogue_gas_payer(). + +Deposit return should go first so epilogue_gas_payer() doesn't have to be aware of this change. + + +
fun epilogue_gas_payer_return_deposit(account: signer, gas_payer: address, storage_fee_refunded: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64, required_deposit: option::Option<u64>)
+
+ + + +
+Implementation + + +
fun epilogue_gas_payer_return_deposit(
+    account: signer,
+    gas_payer: address,
+    storage_fee_refunded: u64,
+    txn_gas_price: u64,
+    txn_max_gas_units: u64,
+    gas_units_remaining: u64,
+    required_deposit: Option<u64>,
+) {
+    return_deposit(gas_payer, required_deposit);
+    epilogue_gas_payer(
+        account,
+        gas_payer,
+        storage_fee_refunded,
+        txn_gas_price,
+        txn_max_gas_units,
+        gas_units_remaining,
+    );
+}
+
+ + +
@@ -763,6 +1021,38 @@ Give some constraints that may abort according to the conditions. + + +### Function `collect_deposit` + + +
fun collect_deposit(gas_payer: address, amount: option::Option<u64>)
+
+ + + + +
pragma verify = false;
+
+ + + + + +### Function `return_deposit` + + +
fun return_deposit(gas_payer: address, amount: option::Option<u64>)
+
+ + + + +
pragma verify = false;
+
+ + + ### Function `prologue_common` @@ -821,6 +1111,22 @@ Give some constraints that may abort according to the conditions. + + +### Function `script_prologue_collect_deposit` + + +
fun script_prologue_collect_deposit(sender: signer, txn_sequence_number: u64, txn_public_key: vector<u8>, txn_gas_price: u64, txn_max_gas_units: u64, txn_expiration_time: u64, chain_id: u8, script_hash: vector<u8>, required_deposit: option::Option<u64>)
+
+ + + + +
pragma verify = false;
+
+ + + ### Function `multi_agent_script_prologue` @@ -898,6 +1204,22 @@ not equal the number of singers. + + +### Function `fee_payer_script_prologue_collect_deposit` + + +
fun fee_payer_script_prologue_collect_deposit(sender: signer, txn_sequence_number: u64, txn_sender_public_key: vector<u8>, secondary_signer_addresses: vector<address>, secondary_signer_public_key_hashes: vector<vector<u8>>, fee_payer_address: address, fee_payer_public_key_hash: vector<u8>, txn_gas_price: u64, txn_max_gas_units: u64, txn_expiration_time: u64, chain_id: u8, required_deposit: option::Option<u64>)
+
+ + + + +
pragma verify = false;
+
+ + + ### Function `epilogue` @@ -917,6 +1239,22 @@ Skip transaction_fee::burn_fee verification. + + +### Function `epilogue_return_deposit` + + +
fun epilogue_return_deposit(account: signer, storage_fee_refunded: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64, required_deposit: option::Option<u64>)
+
+ + + + +
pragma verify = false;
+
+ + + ### Function `epilogue_gas_payer` @@ -1005,4 +1343,20 @@ Skip transaction_fee::burn_fee verification.
+ + + +### Function `epilogue_gas_payer_return_deposit` + + +
fun epilogue_gas_payer_return_deposit(account: signer, gas_payer: address, storage_fee_refunded: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64, required_deposit: option::Option<u64>)
+
+ + + + +
pragma verify = false;
+
+ + [move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/aptos-framework/sources/transaction_validation.move b/aptos-move/framework/aptos-framework/sources/transaction_validation.move index 1997aafdb7b4f..b5797184ca96d 100644 --- a/aptos-move/framework/aptos-framework/sources/transaction_validation.move +++ b/aptos-move/framework/aptos-framework/sources/transaction_validation.move @@ -2,6 +2,8 @@ module aptos_framework::transaction_validation { use std::bcs; use std::error; use std::features; + use std::option; + use std::option::Option; use std::signer; use std::vector; @@ -46,7 +48,7 @@ module aptos_framework::transaction_validation { const PROLOGUE_ESEQUENCE_NUMBER_TOO_BIG: u64 = 1008; const PROLOGUE_ESECONDARY_KEYS_ADDRESSES_COUNT_MISMATCH: u64 = 1009; const PROLOGUE_EFEE_PAYER_NOT_ENABLED: u64 = 1010; - + const PROLOGUE_EINSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT: u64 = 1011; /// Only called during genesis to initialize system resources for this module. public(friend) fun initialize( @@ -70,6 +72,25 @@ module aptos_framework::transaction_validation { }); } + /// Called in prologue to optionally hold some amount for special txns (e.g. randomness txns). + /// `return_deposit()` should be invoked in the corresponding epilogue with the same arguments. + fun collect_deposit(gas_payer: address, amount: Option) { + if (option::is_some(&amount)) { + let amount = option::extract(&mut amount); + let balance = coin::balance(gas_payer); + assert!(balance >= amount, error::invalid_state(PROLOGUE_EINSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT)); + transaction_fee::burn_fee(gas_payer, amount); + } + } + + /// Called in epilogue to optionally released the amount held in prologue for special txns (e.g. randomness txns). + fun return_deposit(gas_payer: address, amount: Option) { + if (option::is_some(&amount)) { + let amount = option::extract(&mut amount); + transaction_fee::mint_and_refund(gas_payer, amount); + } + } + fun prologue_common( sender: signer, gas_payer: address, @@ -152,6 +173,25 @@ module aptos_framework::transaction_validation { prologue_common(sender, gas_payer, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id) } + /// `script_prologue()` then collect an optional deposit depending on the txn. + /// + /// Deposit collection goes last so `script_prologue()` doesn't have to be aware of the deposit logic. + fun script_prologue_collect_deposit( + sender: signer, + txn_sequence_number: u64, + txn_public_key: vector, + txn_gas_price: u64, + txn_max_gas_units: u64, + txn_expiration_time: u64, + chain_id: u8, + script_hash: vector, + required_deposit: Option, + ) { + let gas_payer = signer::address_of(&sender); + script_prologue(sender, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id, script_hash); + collect_deposit(gas_payer, required_deposit); + } + fun multi_agent_script_prologue( sender: signer, txn_sequence_number: u64, @@ -241,6 +281,39 @@ module aptos_framework::transaction_validation { ); } + /// `fee_payer_script_prologue()` then collect an optional deposit depending on the txn. + /// + /// Deposit collection goes last so `fee_payer_script_prologue()` doesn't have to be aware of the deposit logic. + fun fee_payer_script_prologue_collect_deposit( + sender: signer, + txn_sequence_number: u64, + txn_sender_public_key: vector, + secondary_signer_addresses: vector
, + secondary_signer_public_key_hashes: vector>, + fee_payer_address: address, + fee_payer_public_key_hash: vector, + txn_gas_price: u64, + txn_max_gas_units: u64, + txn_expiration_time: u64, + chain_id: u8, + required_deposit: Option, + ) { + fee_payer_script_prologue( + sender, + txn_sequence_number, + txn_sender_public_key, + secondary_signer_addresses, + secondary_signer_public_key_hashes, + fee_payer_address, + fee_payer_public_key_hash, + txn_gas_price, + txn_max_gas_units, + txn_expiration_time, + chain_id, + ); + collect_deposit(fee_payer_address, required_deposit); + } + /// Epilogue function is run after a transaction is successfully executed. /// Called by the Adapter fun epilogue( @@ -254,6 +327,28 @@ module aptos_framework::transaction_validation { epilogue_gas_payer(account, addr, storage_fee_refunded, txn_gas_price, txn_max_gas_units, gas_units_remaining); } + /// Return the deposit held in prologue, then `epilogue()`. + /// + /// Deposit return goes first so `epilogue()` doesn't have to be aware of this change. + fun epilogue_return_deposit( + account: signer, + storage_fee_refunded: u64, + txn_gas_price: u64, + txn_max_gas_units: u64, + gas_units_remaining: u64, + required_deposit: Option, + ) { + let gas_payer = signer::address_of(&account); + return_deposit(gas_payer, required_deposit); + epilogue( + account, + storage_fee_refunded, + txn_gas_price, + txn_max_gas_units, + gas_units_remaining, + ); + } + /// Epilogue function with explicit gas payer specified, is run after a transaction is successfully executed. /// Called by the Adapter fun epilogue_gas_payer( @@ -306,4 +401,27 @@ module aptos_framework::transaction_validation { let addr = signer::address_of(&account); account::increment_sequence_number(addr); } + + /// Return the deposit held in prologue to the gas payer, then `epilogue_gas_payer()`. + /// + /// Deposit return should go first so `epilogue_gas_payer()` doesn't have to be aware of this change. + fun epilogue_gas_payer_return_deposit( + account: signer, + gas_payer: address, + storage_fee_refunded: u64, + txn_gas_price: u64, + txn_max_gas_units: u64, + gas_units_remaining: u64, + required_deposit: Option, + ) { + return_deposit(gas_payer, required_deposit); + epilogue_gas_payer( + account, + gas_payer, + storage_fee_refunded, + txn_gas_price, + txn_max_gas_units, + gas_units_remaining, + ); + } } diff --git a/aptos-move/framework/aptos-framework/sources/transaction_validation.spec.move b/aptos-move/framework/aptos-framework/sources/transaction_validation.spec.move index cc5d0ce6f2624..918dbe02ed31e 100644 --- a/aptos-move/framework/aptos-framework/sources/transaction_validation.spec.move +++ b/aptos-move/framework/aptos-framework/sources/transaction_validation.spec.move @@ -333,6 +333,29 @@ spec aptos_framework::transaction_validation { let aptos_addr = type_info::type_of().account_address; aborts_if (amount_to_mint != 0) && !exists>(aptos_addr); include coin::CoinAddAbortsIf { amount: amount_to_mint }; + } + + spec collect_deposit { + pragma verify = false; + } + + spec return_deposit { + pragma verify = false; + } + + spec fee_payer_script_prologue_collect_deposit { + pragma verify = false; + } + + spec script_prologue_collect_deposit { + pragma verify = false; + } + + spec epilogue_gas_payer_return_deposit { + pragma verify = false; + } + spec epilogue_return_deposit { + pragma verify = false; } } diff --git a/third_party/move/move-core/types/src/vm_status.rs b/third_party/move/move-core/types/src/vm_status.rs index 4ab6783c9ddd0..750866c125dc4 100644 --- a/third_party/move/move-core/types/src/vm_status.rs +++ b/third_party/move/move-core/types/src/vm_status.rs @@ -583,11 +583,13 @@ pub enum StatusCode { MULTISIG_TRANSACTION_INSUFFICIENT_APPROVALS = 34, MULTISIG_TRANSACTION_PAYLOAD_DOES_NOT_MATCH_HASH = 35, GAS_PAYER_ACCOUNT_MISSING = 36, + INSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT = 37, + GAS_PARAMS_MISSING = 38, // Reserved error code for future use - RESERVED_VALIDATION_ERROR_2 = 37, - RESERVED_VALIDATION_ERROR_3 = 38, RESERVED_VALIDATION_ERROR_4 = 39, RESERVED_VALIDATION_ERROR_5 = 40, + RESERVED_VALIDATION_ERROR_6 = 41, + RESERVED_VALIDATION_ERROR_7 = 42, // When a code module/script is published it is verified. These are the // possible errors that can arise from the verification process.