From 70f40278ba760793f7bc20535f0773b270d373f6 Mon Sep 17 00:00:00 2001 From: Aregnaz Harutyunyan <89187359+aregng@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:37:34 +0400 Subject: [PATCH] [AN-Issue-1347] Implemented Automated transaction execution flow (#136) * [AN-Issue-1347] Implemented Automated transasction execution flow * Updated automated task prologue and added e2e tests - Added chain-id verification and task availability check by task index - Added e2e tests for AutoamtedTransaction * Fixed test failure * Added task sender check in automated_transaction_prologue --------- Co-authored-by: Aregnaz Harutyunyan <> --- aptos-move/aptos-vm/src/aptos_vm.rs | 32 +- .../src/automated_transaction_processor.rs | 480 ++++++++++++++++++ aptos-move/aptos-vm/src/errors.rs | 10 +- aptos-move/aptos-vm/src/lib.rs | 1 + .../aptos-vm/src/transaction_validation.rs | 141 +++++ aptos-move/e2e-tests/src/executor.rs | 34 +- .../src/tests/automated_transactions.rs | 177 +++++++ .../src/tests/automation_registration.rs | 92 +++- aptos-move/e2e-testsuite/src/tests/mod.rs | 1 + .../doc/automation_registry.md | 12 +- .../doc/automation_registry_state.md | 16 +- .../doc/transaction_validation.md | 144 +++++- .../sources/automation_registry.move | 4 +- .../sources/automation_registry_state.move | 8 +- .../sources/transaction_validation.move | 69 ++- .../move/move-core/types/src/vm_status.rs | 4 + 16 files changed, 1172 insertions(+), 53 deletions(-) create mode 100644 aptos-move/aptos-vm/src/automated_transaction_processor.rs create mode 100644 aptos-move/e2e-testsuite/src/tests/automated_transactions.rs diff --git a/aptos-move/aptos-vm/src/aptos_vm.rs b/aptos-move/aptos-vm/src/aptos_vm.rs index d6b36c884f794..b0c5ae09e2157 100644 --- a/aptos-move/aptos-vm/src/aptos_vm.rs +++ b/aptos-move/aptos-vm/src/aptos_vm.rs @@ -109,6 +109,7 @@ use std::{ marker::Sync, sync::Arc, }; +use crate::automated_transaction_processor::AutomatedTransactionProcessor; static EXECUTION_CONCURRENCY_LEVEL: OnceCell = OnceCell::new(); static NUM_EXECUTION_SHARD: OnceCell = OnceCell::new(); @@ -151,6 +152,8 @@ macro_rules! unwrap_or_discard { }; } +pub(crate) use unwrap_or_discard; + pub(crate) fn get_system_transaction_output( session: SessionExt, fee_statement: FeeStatement, @@ -271,7 +274,7 @@ impl AptosVM { } #[inline(always)] - fn features(&self) -> &Features { + pub(crate) fn features(&self) -> &Features { self.move_vm.env.features() } @@ -397,7 +400,7 @@ impl AptosVM { ) } - fn fee_statement_from_gas_meter( + pub(crate) fn fee_statement_from_gas_meter( txn_data: &TransactionMetadata, gas_meter: &impl AptosGasMeter, storage_fee_refund: u64, @@ -488,7 +491,7 @@ impl AptosVM { } } - fn inject_abort_info_if_available(&self, status: ExecutionStatus) -> ExecutionStatus { + pub(crate) fn inject_abort_info_if_available(&self, status: ExecutionStatus) -> ExecutionStatus { match status { ExecutionStatus::MoveAbort { location: AbortLocation::Module(module), @@ -747,7 +750,7 @@ impl AptosVM { Ok(()) } - fn validate_and_execute_entry_function( + pub(crate) fn validate_and_execute_entry_function( &self, resolver: &impl AptosMoveResolver, session: &mut SessionExt, @@ -1053,7 +1056,7 @@ impl AptosVM { Ok(storage_refund) } - fn charge_change_set_and_respawn_session<'r, 'l>( + pub(crate) fn charge_change_set_and_respawn_session<'r, 'l>( &'l self, user_session: UserSession<'r, 'l>, resolver: &'r impl AptosMoveResolver, @@ -1506,7 +1509,7 @@ impl AptosVM { } /// Resolve a pending code publish request registered via the NativeCodeContext. - fn resolve_pending_code_publish( + pub(crate) fn resolve_pending_code_publish( &self, session: &mut SessionExt, gas_meter: &mut impl AptosGasMeter, @@ -1923,6 +1926,7 @@ impl AptosVM { }) } + /// Main entrypoint for executing a user transaction that also allows the customization of the /// gas meter to be used. pub fn execute_user_transaction_with_custom_gas_meter( @@ -2596,11 +2600,23 @@ impl AptosVM { self.process_validator_transaction(resolver, txn.clone(), log_context)?; (vm_status, output) }, - Transaction::AutomatedTransaction(_) => { - unimplemented!("AutomatedTransaction execution is coming soon") + Transaction::AutomatedTransaction(txn) => { + AutomatedTransactionProcessor::new(self).execute_transaction(resolver, txn, log_context) }, }) } + + pub(crate) fn gas_params_internal(&self) -> &Result { + &self.gas_params + } + + pub(crate) fn move_vm(&self) -> &MoveVmExt { + &self.move_vm + } + + pub(crate) fn gas_feature_version(&self) -> u64 { + self.gas_feature_version + } } // Executor external API diff --git a/aptos-move/aptos-vm/src/automated_transaction_processor.rs b/aptos-move/aptos-vm/src/automated_transaction_processor.rs new file mode 100644 index 0000000000000..3d088031fd744 --- /dev/null +++ b/aptos-move/aptos-vm/src/automated_transaction_processor.rs @@ -0,0 +1,480 @@ +// Copyright (c) 2024 Supra. +// SPDX-License-Identifier: Apache-2.0 + +use crate::aptos_vm::{get_or_vm_startup_failure, unwrap_or_discard}; +use crate::counters::TXN_GAS_USAGE; +use crate::errors::discarded_output; +use crate::gas::{check_gas, make_prod_gas_meter}; +use crate::move_vm_ext::session::user_transaction_sessions::epilogue::EpilogueSession; +use crate::move_vm_ext::session::user_transaction_sessions::prologue::PrologueSession; +use crate::move_vm_ext::session::user_transaction_sessions::user::UserSession; +use crate::move_vm_ext::{AptosMoveResolver, SessionExt}; +use crate::transaction_metadata::TransactionMetadata; +use crate::{transaction_validation, AptosVM}; +use aptos_gas_algebra::Gas; +use aptos_gas_meter::{AptosGasMeter, GasAlgebra}; +use aptos_gas_schedule::VMGasParameters; +use aptos_types::fee_statement::FeeStatement; +use aptos_types::on_chain_config::FeatureFlag; +use aptos_types::transaction::automated_transaction::AutomatedTransaction; +use aptos_types::transaction::{EntryFunction, ExecutionStatus, TransactionAuxiliaryData, TransactionPayload, TransactionStatus}; +use aptos_vm_logging::log_schema::AdapterLogSchema; +use aptos_vm_types::change_set::VMChangeSet; +use aptos_vm_types::output::VMOutput; +use aptos_vm_types::storage::change_set_configs::ChangeSetConfigs; +use aptos_vm_types::storage::StorageGasParameters; +use fail::fail_point; +use move_core_types::vm_status::{StatusCode, VMStatus}; +use move_vm_runtime::module_traversal::{TraversalContext, TraversalStorage}; +use std::ops::Deref; +use move_binary_format::errors::Location; + +pub struct AutomatedTransactionProcessor<'m> { + aptos_vm: &'m AptosVM, +} + +impl Deref for AutomatedTransactionProcessor<'_> { + type Target = AptosVM; + + fn deref(&self) -> &Self::Target { + self.aptos_vm + } +} + +impl<'m> AutomatedTransactionProcessor<'m> { + pub(crate) fn new(aptos_vm: &'m AptosVM) -> Self { + Self { aptos_vm } + } + + fn validate_automated_transaction( + &self, + session: &mut SessionExt, + resolver: &impl AptosMoveResolver, + transaction: &AutomatedTransaction, + transaction_data: &TransactionMetadata, + log_context: &AdapterLogSchema, + traversal_context: &mut TraversalContext, + ) -> Result<(), VMStatus> { + let TransactionPayload::EntryFunction(_entry_function) = transaction.payload() else { + return Err(VMStatus::error(StatusCode::INVALID_AUTOMATED_PAYLOAD, None)); + }; + check_gas( + get_or_vm_startup_failure(&self.gas_params_internal(), log_context)?, + self.gas_feature_version(), + resolver, + transaction_data, + self.features(), + false, + log_context, + )?; + + transaction_validation::run_automated_transaction_prologue( + session, + transaction_data, + log_context, + traversal_context, + ) + } + + fn success_transaction_cleanup( + &self, + mut epilogue_session: EpilogueSession, + gas_meter: &impl AptosGasMeter, + txn_data: &TransactionMetadata, + log_context: &AdapterLogSchema, + change_set_configs: &ChangeSetConfigs, + traversal_context: &mut TraversalContext, + ) -> Result<(VMStatus, VMOutput), VMStatus> + { + if self.gas_feature_version() >= 12 { + // Check if the gas meter's internal counters are consistent. + // + // It's better to fail the transaction due to invariant violation than to allow + // potentially bogus states to be committed. + if let Err(err) = gas_meter.algebra().check_consistency() { + println!( + "[aptos-vm][gas-meter][success-epilogue] {}", + err.message() + .unwrap_or("No message found -- this should not happen.") + ); + return Err(err.finish(Location::Undefined).into()); + } + } + + let fee_statement = AptosVM::fee_statement_from_gas_meter( + txn_data, + gas_meter, + u64::from(epilogue_session.get_storage_fee_refund()), + ); + epilogue_session.execute(|session| { + transaction_validation::run_automated_txn_success_epilogue( + session, + gas_meter.balance(), + fee_statement, + self.features(), + txn_data, + log_context, + traversal_context, + ) + })?; + let change_set = epilogue_session.finish(change_set_configs)?; + let output = VMOutput::new( + change_set, + fee_statement, + TransactionStatus::Keep(ExecutionStatus::Success), + TransactionAuxiliaryData::default(), + ); + + Ok((VMStatus::Executed, output)) + } + + fn executed_entry_function<'a, 'r, 'l>( + &'l self, + resolver: &'r impl AptosMoveResolver, + mut session: UserSession<'r, 'l>, + gas_meter: &mut impl AptosGasMeter, + traversal_context: &mut TraversalContext<'a>, + txn_data: &TransactionMetadata, + entry_function: &'a EntryFunction, + log_context: &AdapterLogSchema, + new_published_modules_loaded: &mut bool, + change_set_configs: &ChangeSetConfigs, + ) -> Result<(VMStatus, VMOutput), VMStatus> { + fail_point!( + "aptos_vm::automated_transaction_processor::execute_payload", + |_| { + Err(VMStatus::Error { + status_code: StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + sub_status: Some(move_core_types::vm_status::sub_status::unknown_invariant_violation::EPARANOID_FAILURE), + message: None, + }) + } + ); + + gas_meter.charge_intrinsic_gas_for_transaction(txn_data.transaction_size())?; + session.execute(|session| { + self.validate_and_execute_entry_function( + resolver, + session, + gas_meter, + traversal_context, + txn_data.senders(), + entry_function, + txn_data, + ) + })?; + + session.execute(|session| { + self.resolve_pending_code_publish( + session, + gas_meter, + traversal_context, + new_published_modules_loaded, + ) + })?; + + let epilogue_session = self.charge_change_set_and_respawn_session( + session, + resolver, + gas_meter, + change_set_configs, + txn_data, + )?; + + self.success_transaction_cleanup( + epilogue_session, + gas_meter, + txn_data, + log_context, + change_set_configs, + traversal_context, + ) + } + + // Called when the execution of the transaction fails, in order to discard the + // transaction, or clean up the failed state. + fn on_transaction_execution_failure( + &self, + prologue_change_set: VMChangeSet, + err: VMStatus, + resolver: &impl AptosMoveResolver, + txn_data: &TransactionMetadata, + log_context: &AdapterLogSchema, + gas_meter: &mut impl AptosGasMeter, + change_set_configs: &ChangeSetConfigs, + new_published_modules_loaded: bool, + traversal_context: &mut TraversalContext, + ) -> (VMStatus, VMOutput) { + // Invalidate the loader cache in case there was a new module loaded from a module + // publish request that failed. + // This ensures the loader cache is flushed later to align storage with the cache. + // None of the modules in the bundle will be committed to storage, + // but some of them may have ended up in the cache. + if new_published_modules_loaded { + self.move_vm().mark_loader_cache_as_invalid(); + }; + + self.failed_transaction_cleanup( + prologue_change_set, + err, + gas_meter, + txn_data, + resolver, + log_context, + change_set_configs, + traversal_context, + ) + } + pub(crate) fn execute_transaction_impl<'a>( + &self, + resolver: &impl AptosMoveResolver, + txn: &AutomatedTransaction, + txn_data: TransactionMetadata, + gas_meter: &mut impl AptosGasMeter, + log_context: &AdapterLogSchema, + ) -> (VMStatus, VMOutput) { + let traversal_storage = TraversalStorage::new(); + let mut traversal_context = TraversalContext::new(&traversal_storage); + + // Revalidate the transaction. + let mut prologue_session = + unwrap_or_discard!(PrologueSession::new(self.aptos_vm, &txn_data, resolver)); + + let exec_result = prologue_session.execute(|session| { + self.validate_automated_transaction( + session, + resolver, + txn, + &txn_data, + log_context, + &mut traversal_context, + ) + }); + unwrap_or_discard!(exec_result); + let storage_gas_params = unwrap_or_discard!(get_or_vm_startup_failure( + &self.storage_gas_params, + log_context + )); + let change_set_configs = &storage_gas_params.change_set_configs; + let (prologue_change_set, user_session) = unwrap_or_discard!(prologue_session + .into_user_session( + self, + &txn_data, + resolver, + self.gas_feature_version(), + change_set_configs, + )); + let TransactionPayload::EntryFunction(automated_entry_function) = txn.payload() else { + return ( + VMStatus::error(StatusCode::INVALID_AUTOMATED_PAYLOAD, None), + discarded_output(StatusCode::INVALID_AUTOMATED_PAYLOAD), + ); + }; + + // // We keep track of whether any newly published modules are loaded into the Vm's loader + // // cache as part of executing transactions. This would allow us to decide whether the cache + // // should be flushed later. + let mut new_published_modules_loaded = false; + let result = self.executed_entry_function( + resolver, + user_session, + gas_meter, + &mut traversal_context, + &txn_data, + automated_entry_function, + log_context, + &mut new_published_modules_loaded, + change_set_configs, + ); + + let gas_usage = txn_data + .max_gas_amount() + .checked_sub(gas_meter.balance()) + .expect("Balance should always be less than or equal to max gas amount set"); + TXN_GAS_USAGE.observe(u64::from(gas_usage) as f64); + + result.unwrap_or_else(|err| { + self.on_transaction_execution_failure( + prologue_change_set, + err, + resolver, + &txn_data, + log_context, + gas_meter, + change_set_configs, + new_published_modules_loaded, + &mut traversal_context, + ) + }) + } + + /// Main entrypoint for executing a user transaction that also allows the customization of the + /// gas meter to be used. + pub fn execute_transaction_with_custom_gas_meter( + &self, + resolver: &impl AptosMoveResolver, + txn: &AutomatedTransaction, + log_context: &AdapterLogSchema, + make_gas_meter: F, + ) -> Result<(VMStatus, VMOutput, G), VMStatus> + where + G: AptosGasMeter, + F: FnOnce(u64, VMGasParameters, StorageGasParameters, bool, Gas) -> G, + { + let txn_metadata = TransactionMetadata::from(txn); + + let balance = txn.max_gas_amount().into(); + let mut gas_meter = make_gas_meter( + self.gas_feature_version(), + get_or_vm_startup_failure(&self.gas_params_internal(), log_context)? + .vm + .clone(), + get_or_vm_startup_failure(&self.storage_gas_params, log_context)?.clone(), + false, + balance, + ); + let (status, output) = + self.execute_transaction_impl(resolver, txn, txn_metadata, &mut gas_meter, log_context); + + Ok((status, output, gas_meter)) + } + + /// Executes an automated transaction using the production gas meter. + pub fn execute_transaction( + &self, + resolver: &impl AptosMoveResolver, + txn: &AutomatedTransaction, + log_context: &AdapterLogSchema, + ) -> (VMStatus, VMOutput) { + match self.execute_transaction_with_custom_gas_meter( + resolver, + txn, + log_context, + make_prod_gas_meter, + ) { + Ok((vm_status, vm_output, _gas_meter)) => (vm_status, vm_output), + Err(vm_status) => { + let vm_output = discarded_output(vm_status.status_code()); + (vm_status, vm_output) + }, + } + } + + fn failed_transaction_cleanup( + &self, + prologue_change_set: VMChangeSet, + error_vm_status: VMStatus, + gas_meter: &mut impl AptosGasMeter, + txn_data: &TransactionMetadata, + resolver: &impl AptosMoveResolver, + log_context: &AdapterLogSchema, + change_set_configs: &ChangeSetConfigs, + traversal_context: &mut TraversalContext, + ) -> (VMStatus, VMOutput) { + if self.gas_feature_version() >= 12 { + // Check if the gas meter's internal counters are consistent. + // + // Since we are already in the failure epilogue, there is not much we can do + // other than logging the inconsistency. + // + // This is a tradeoff. We have to either + // 1. Continue to calculate the gas cost based on the numbers we have. + // 2. Discard the transaction. + // + // Option (2) does not work, since it would enable DoS attacks. + // Option (1) is not ideal, but optimistically, it should allow the network + // to continue functioning, less the transactions that run into this problem. + if let Err(err) = gas_meter.algebra().check_consistency() { + println!( + "[aptos-vm][gas-meter][failure-epilogue] {}", + err.message() + .unwrap_or("No message found -- this should not happen.") + ); + } + } + + let (txn_status, txn_aux_data) = TransactionStatus::from_vm_status( + error_vm_status.clone(), + self.features() + .is_enabled(FeatureFlag::CHARGE_INVARIANT_VIOLATION), + self.features(), + ); + + match txn_status { + TransactionStatus::Keep(status) => { + // The transaction should be kept. Run the appropriate post transaction workflows + // including epilogue. This runs a new session that ignores any side effects that + // might abort the execution (e.g., spending additional funds needed to pay for + // gas). Even if the previous failure occurred while running the epilogue, it + // should not fail now. If it somehow fails here, there is no choice but to + // discard the transaction. + let txn_output = match self.finish_aborted_transaction( + prologue_change_set, + gas_meter, + txn_data, + resolver, + status, + log_context, + change_set_configs, + traversal_context, + ) { + Ok((change_set, fee_statement, status)) => VMOutput::new( + change_set, + fee_statement, + TransactionStatus::Keep(status), + txn_aux_data, + ), + Err(err) => discarded_output(err.status_code()), + }; + (error_vm_status, txn_output) + }, + TransactionStatus::Discard(status_code) => { + let discarded_output = discarded_output(status_code); + (error_vm_status, discarded_output) + }, + TransactionStatus::Retry => unreachable!(), + } + } + + fn finish_aborted_transaction( + &self, + prologue_change_set: VMChangeSet, + gas_meter: &mut impl AptosGasMeter, + txn_data: &TransactionMetadata, + resolver: &impl AptosMoveResolver, + status: ExecutionStatus, + log_context: &AdapterLogSchema, + change_set_configs: &ChangeSetConfigs, + traversal_context: &mut TraversalContext, + ) -> Result<(VMChangeSet, FeeStatement, ExecutionStatus), VMStatus> { + // Storage refund is zero since no slots are deleted in aborted transactions. + const ZERO_STORAGE_REFUND: u64 = 0; + + let mut epilogue_session = EpilogueSession::new( + self, + txn_data, + resolver, + prologue_change_set, + ZERO_STORAGE_REFUND.into(), + )?; + + let status = self.inject_abort_info_if_available(status); + + let fee_statement = + AptosVM::fee_statement_from_gas_meter(txn_data, gas_meter, ZERO_STORAGE_REFUND); + epilogue_session.execute(|session| { + transaction_validation::run_automated_txn_failure_epilogue( + session, + gas_meter.balance(), + fee_statement, + self.features(), + txn_data, + log_context, + traversal_context, + ) + })?; + epilogue_session + .finish(change_set_configs) + .map(|set| (set, fee_statement, status)) + } +} diff --git a/aptos-move/aptos-vm/src/errors.rs b/aptos-move/aptos-vm/src/errors.rs index 629c576875b37..0eb49cc3f585b 100644 --- a/aptos-move/aptos-vm/src/errors.rs +++ b/aptos-move/aptos-vm/src/errors.rs @@ -1,3 +1,4 @@ +// Copyright (c) 2024 Supra. // Copyright © Aptos Foundation // Parts of the project are originally copyright © Meta Platforms, Inc. // SPDX-License-Identifier: Apache-2.0 @@ -38,6 +39,9 @@ pub const EGAS_PAYER_ACCOUNT_MISSING: u64 = 1010; // Insufficient balance to cover the required deposit. pub const EINSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT: u64 = 1011; +// Failed to find active automation task by specified id. +pub const ENO_ACTIVE_AUTOMATION_TASK: u64 = 1012; + // Specified account is not a multisig account. const EACCOUNT_NOT_MULTISIG: u64 = 2002; // Account executing this operation is not an owner of the multisig account. @@ -106,7 +110,8 @@ pub fn convert_prologue_error( VMStatus::error(new_major_status, None) }, VMStatus::MoveAbort(location, code) => { - let new_major_status = match error_split(code) { + let (category, ccode) = error_split(code); + let new_major_status = match (category, ccode) { // Invalid authentication key (INVALID_ARGUMENT, EBAD_ACCOUNT_AUTHENTICATION_KEY) => StatusCode::INVALID_AUTH_KEY, // Sequence number too old @@ -134,6 +139,9 @@ pub fn convert_prologue_error( (INVALID_STATE, EINSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT) => { StatusCode::INSUFFICIENT_BALANCE_FOR_REQUIRED_DEPOSIT }, + (INVALID_STATE, ENO_ACTIVE_AUTOMATION_TASK) => { + StatusCode::NO_ACTIVE_AUTOMATED_TASK + }, (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/lib.rs b/aptos-move/aptos-vm/src/lib.rs index 20c3caff2dc29..f332a861e0b4f 100644 --- a/aptos-move/aptos-vm/src/lib.rs +++ b/aptos-move/aptos-vm/src/lib.rs @@ -123,6 +123,7 @@ pub mod transaction_metadata; mod transaction_validation; pub mod validator_txns; pub mod verifier; +mod automated_transaction_processor; pub use crate::aptos_vm::{AptosSimulationVM, AptosVM}; use crate::sharded_block_executor::{executor_client::ExecutorClient, ShardedBlockExecutor}; diff --git a/aptos-move/aptos-vm/src/transaction_validation.rs b/aptos-move/aptos-vm/src/transaction_validation.rs index c90f7bd8c43e0..ff989a647ddd8 100644 --- a/aptos-move/aptos-vm/src/transaction_validation.rs +++ b/aptos-move/aptos-vm/src/transaction_validation.rs @@ -38,8 +38,10 @@ pub static APTOS_TRANSACTION_VALIDATION: Lazy = fee_payer_prologue_name: Identifier::new("fee_payer_script_prologue").unwrap(), script_prologue_name: Identifier::new("script_prologue").unwrap(), multi_agent_prologue_name: Identifier::new("multi_agent_script_prologue").unwrap(), + automated_txn_prologue_name: Identifier::new("automated_transaction_prologue").unwrap(), user_epilogue_name: Identifier::new("epilogue").unwrap(), user_epilogue_gas_payer_name: Identifier::new("epilogue_gas_payer").unwrap(), + automated_txn_epilogue_name: Identifier::new("automated_transaction_epilogue").unwrap(), }); /// On-chain functions used to validate transactions @@ -50,8 +52,10 @@ pub struct TransactionValidation { pub fee_payer_prologue_name: Identifier, pub script_prologue_name: Identifier, pub multi_agent_prologue_name: Identifier, + pub automated_txn_prologue_name: Identifier, pub user_epilogue_name: Identifier, pub user_epilogue_gas_payer_name: Identifier, + pub automated_txn_epilogue_name: Identifier, } impl TransactionValidation { @@ -191,6 +195,44 @@ pub(crate) fn run_multisig_prologue( .or_else(|err| convert_prologue_error(err, log_context)) } +/// Run the prologue for automated transactions where +/// 1. sender is checked to have enough funds for transaction execution +/// 2. automated task expiry time is checked. +pub(crate) fn run_automated_transaction_prologue( + session: &mut SessionExt, + txn_data: &TransactionMetadata, + log_context: &AdapterLogSchema, + traversal_context: &mut TraversalContext, +) -> Result<(), VMStatus> { + let txn_task_id = txn_data.sequence_number(); + let txn_gas_price = txn_data.gas_unit_price(); + let txn_max_gas_units = txn_data.max_gas_amount(); + let txn_expiration_timestamp_secs = txn_data.expiration_timestamp_secs(); + let chain_id = txn_data.chain_id(); + let mut gas_meter = UnmeteredGasMeter; + let args = vec![ + MoveValue::Signer(txn_data.sender), + MoveValue::U64(txn_task_id), + MoveValue::U64(txn_gas_price.into()), + MoveValue::U64(txn_max_gas_units.into()), + MoveValue::U64(txn_expiration_timestamp_secs), + MoveValue::U8(chain_id.id()), + ]; + session + .execute_function_bypass_visibility( + &APTOS_TRANSACTION_VALIDATION.module_id(), + &APTOS_TRANSACTION_VALIDATION.automated_txn_prologue_name, + vec![], + serialize_values(&args), + &mut gas_meter, + traversal_context, + ) + .map(|_return_vals| ()) + .map_err(expect_no_verification_errors) + .or_else(|err| convert_prologue_error(err, log_context)) +} + + fn run_epilogue( session: &mut SessionExt, gas_remaining: Gas, @@ -261,6 +303,48 @@ fn run_epilogue( Ok(()) } +fn run_automated_txn_epilogue( + session: &mut SessionExt, + gas_remaining: Gas, + fee_statement: FeeStatement, + txn_data: &TransactionMetadata, + features: &Features, + traversal_context: &mut TraversalContext, +) -> VMResult<()> { + let txn_gas_price = txn_data.gas_unit_price(); + let txn_max_gas_units = txn_data.max_gas_amount(); + + // 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. + // Regular tx, run the normal epilogue + let 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()), + ]; + session.execute_function_bypass_visibility( + &APTOS_TRANSACTION_VALIDATION.module_id(), + &APTOS_TRANSACTION_VALIDATION.automated_txn_epilogue_name, + vec![], + serialize_values(&args), + &mut UnmeteredGasMeter, + traversal_context, + ) + .map(|_return_vals| ()) + .map_err(expect_no_verification_errors)?; + + // Emit the FeeStatement event + if features.is_emit_fee_statement_enabled() { + emit_fee_statement(session, fee_statement, traversal_context)?; + } + + maybe_raise_injected_error(InjectedError::EndOfRunEpilogue)?; + + Ok(()) +} + fn emit_fee_statement( session: &mut SessionExt, fee_statement: FeeStatement, @@ -307,6 +391,35 @@ pub(crate) fn run_success_epilogue( .or_else(|err| convert_epilogue_error(err, log_context)) } +/// Run the epilogue of a transaction by calling into `AUTOMATED_TXN_EPILOGUE_NAME` function stored +/// in the `TRANSACTION_VALIDATION_MODULE` on chain. +pub(crate) fn run_automated_txn_success_epilogue( + session: &mut SessionExt, + gas_remaining: Gas, + fee_statement: FeeStatement, + features: &Features, + txn_data: &TransactionMetadata, + log_context: &AdapterLogSchema, + traversal_context: &mut TraversalContext, +) -> Result<(), VMStatus> { + fail_point!("move_adapter::run_automated_txn_success_epilogue", |_| { + Err(VMStatus::error( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + None, + )) + }); + + run_automated_txn_epilogue( + session, + gas_remaining, + fee_statement, + txn_data, + features, + traversal_context, + ) + .or_else(|err| convert_epilogue_error(err, log_context)) +} + /// Run the failure epilogue of a transaction by calling into `USER_EPILOGUE_NAME` function /// stored in the `ACCOUNT_MODULE` on chain. pub(crate) fn run_failure_epilogue( @@ -334,3 +447,31 @@ pub(crate) fn run_failure_epilogue( ) }) } + +/// Run the failure epilogue of a transaction by calling into `AUTOMATED_TXN_EPILOGUE_NAME` function +/// stored in the `TRANSACTION_VALIDATION_MODULE` on chain. +pub(crate) fn run_automated_txn_failure_epilogue( + session: &mut SessionExt, + gas_remaining: Gas, + fee_statement: FeeStatement, + features: &Features, + txn_data: &TransactionMetadata, + log_context: &AdapterLogSchema, + traversal_context: &mut TraversalContext, +) -> Result<(), VMStatus> { + run_automated_txn_epilogue( + session, + gas_remaining, + fee_statement, + txn_data, + features, + traversal_context, + ) + .or_else(|e| { + expect_only_successful_execution( + e, + APTOS_TRANSACTION_VALIDATION.automated_txn_epilogue_name.as_str(), + log_context, + ) + }) +} diff --git a/aptos-move/e2e-tests/src/executor.rs b/aptos-move/e2e-tests/src/executor.rs index a18cfa4cabb26..83b3ddcbe7f1f 100644 --- a/aptos-move/e2e-tests/src/executor.rs +++ b/aptos-move/e2e-tests/src/executor.rs @@ -496,11 +496,28 @@ impl FakeExecutor { /// Executes the transaction as a singleton block and applies the resulting write set to the /// data store. Panics if execution fails pub fn execute_and_apply(&mut self, transaction: SignedTransaction) -> TransactionOutput { - let mut outputs = self.execute_block(vec![transaction]).unwrap(); - assert!(outputs.len() == 1, "transaction outputs size mismatch"); + self.execute_and_apply_transaction(Transaction::UserTransaction(transaction)) + } + + /// Executes the transaction as a singleton block and applies the resulting write set to the + /// data store. Panics if execution fails + pub fn execute_and_apply_transaction(&mut self, transaction: Transaction) -> TransactionOutput { + let mut outputs = self.execute_transaction_block(vec![transaction]).unwrap(); + assert_eq!(outputs.len() , 1, "transaction outputs size mismatch"); let output = outputs.pop().unwrap(); match output.status() { TransactionStatus::Keep(status) => { + match status { + ExecutionStatus::Success => {} + ExecutionStatus::OutOfGas => {} + ExecutionStatus::MoveAbort { code,.. } => { + let reason = code & 0xFFFF; + let category = ((code >> 16) & 0xFF) as u8; + println!("{category}: {reason}"); + } + ExecutionStatus::ExecutionFailure { .. } => {} + ExecutionStatus::MiscellaneousError(_) => {} + } self.apply_write_set(output.write_set()); assert_eq!( status, @@ -515,6 +532,7 @@ impl FakeExecutor { } } + fn execute_transaction_block_impl_with_state_view( &self, txn_block: &[SignatureVerifiedTransaction], @@ -664,6 +682,18 @@ impl FakeExecutor { txn_output } + pub fn execute_tagged_transaction(&self, txn: Transaction) -> TransactionOutput { + let txn_block = vec![txn]; + let mut outputs = self + .execute_transaction_block(txn_block) + .expect("The VM should not fail to startup"); + let mut txn_output = outputs + .pop() + .expect("A block with one transaction should have one output"); + txn_output.fill_error_status(); + txn_output + } + pub fn execute_transaction_with_gas_profiler( &self, txn: SignedTransaction, diff --git a/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs b/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs new file mode 100644 index 0000000000000..c3366ca452cea --- /dev/null +++ b/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs @@ -0,0 +1,177 @@ +// Copyright (c) 2024 Supra. +// SPDX-License-Identifier: Apache-2.0 + +use crate::tests::automation_registration::AutomationRegistrationTestContext; +use aptos_cached_packages::{aptos_framework_sdk_builder}; +use aptos_crypto::HashValue; +use aptos_types::transaction::automated_transaction::AutomatedTransaction; +use aptos_types::transaction::{ExecutionStatus, Transaction, TransactionStatus}; +use aptos_vm::transaction_metadata::TransactionMetadata; +use move_core_types::vm_status::StatusCode; + +#[test] +fn check_unregistered_automated_transaction() { + let mut test_context = AutomationRegistrationTestContext::new(); + let dest_account = test_context.new_account_data(0, 0); + let payload = aptos_framework_sdk_builder::supra_coin_mint(dest_account.address().clone(), 100); + let sequence_number = 0; + + let raw_transaction = test_context + .sender_account_data() + .account() + .transaction() + .payload(payload) + .sequence_number(sequence_number) + .ttl(4000) + .raw(); + let parent_has = HashValue::new([42; HashValue::LENGTH]); + let automated_txn = AutomatedTransaction::new(raw_transaction.clone(), parent_has, 1); + let result = + test_context.execute_tagged_transaction(Transaction::AutomatedTransaction(automated_txn)); + AutomationRegistrationTestContext::check_discarded_output( + result, + StatusCode::NO_ACTIVE_AUTOMATED_TASK, + ); +} + +#[test] +fn check_expired_automated_transaction() { + let mut test_context = AutomationRegistrationTestContext::new(); + test_context.advance_chain_time_in_secs(2500); + let dest_account = test_context.new_account_data(0, 0); + let payload = aptos_framework_sdk_builder::supra_coin_mint(dest_account.address().clone(), 100); + let sequence_number = 0; + + let raw_transaction = test_context + .sender_account_data() + .account() + .transaction() + .payload(payload) + .sequence_number(sequence_number) + .ttl(1000) + .raw(); + + let parent_hash = HashValue::new([42; HashValue::LENGTH]); + let automated_txn = AutomatedTransaction::new(raw_transaction.clone(), parent_hash, 1); + let result = + test_context.execute_tagged_transaction(Transaction::AutomatedTransaction(automated_txn)); + AutomationRegistrationTestContext::check_discarded_output( + result, + StatusCode::TRANSACTION_EXPIRED, + ); +} + +#[test] +fn check_automated_transaction_with_insufficient_balance() { + let mut test_context = AutomationRegistrationTestContext::new(); + let dest_account = test_context.new_account_data(0, 0); + let payload = aptos_framework_sdk_builder::supra_account_transfer(dest_account.address().clone(), 100); + let sequence_number = 0; + + let raw_transaction = test_context + .sender_account_data() + .account() + .transaction() + .payload(payload) + .sequence_number(sequence_number) + .gas_unit_price(1_000_000) + .max_gas_amount(1_000_000) + .ttl(1000) + .raw(); + + let parent_hash = HashValue::new([42; HashValue::LENGTH]); + let automated_txn = AutomatedTransaction::new(raw_transaction.clone(), parent_hash, 1); + let result = + test_context.execute_tagged_transaction(Transaction::AutomatedTransaction(automated_txn)); + AutomationRegistrationTestContext::check_discarded_output( + result, + StatusCode::INSUFFICIENT_BALANCE_FOR_TRANSACTION_FEE, + ); +} + +#[test] +fn check_automated_transaction_successful_execution() { + let mut test_context = AutomationRegistrationTestContext::new(); + let dest_account = test_context.new_account_data(1_000_000, 0); + let payload = aptos_framework_sdk_builder::supra_account_transfer(dest_account.address().clone(), 100); + let gas_price = 100; + let max_gas_amount = 100; + + // Register automation task + let inner_entry_function = payload.clone().into_entry_function(); + let expiration_time = test_context.chain_time_now() + 8000; + let automation_txn = test_context.create_automation_txn( + 0, + inner_entry_function.clone(), + expiration_time, + gas_price, + max_gas_amount, + ); + + let output = test_context.execute_and_apply(automation_txn); + assert_eq!( + output.status(), + &TransactionStatus::Keep(ExecutionStatus::Success), + "{output:?}" + ); + + // For now no active transaction available, so this task execution will fail on prologue. + let sequence_number = 0; + let raw_transaction = test_context + .sender_account_data() + .account() + .transaction() + .payload(payload.clone()) + .sequence_number(sequence_number) + .gas_unit_price(gas_price) + .max_gas_amount(max_gas_amount) + .ttl(expiration_time) + .raw(); + + let parent_hash = HashValue::new([42; HashValue::LENGTH]); + let automated_txn = AutomatedTransaction::new(raw_transaction.clone(), parent_hash, 1); + let result = + test_context.execute_tagged_transaction(Transaction::AutomatedTransaction(automated_txn.clone())); + AutomationRegistrationTestContext::check_discarded_output( + result, + StatusCode::NO_ACTIVE_AUTOMATED_TASK, + ); + + // Moving to the next epoch + test_context.advance_chain_time_in_secs(7200); + + // Execute automated transaction one more time which should be success, as task is already become active after epoch change + let sender_address = test_context.sender_account_address(); + let sender_seq_num = test_context.account_sequence_number(sender_address); + let output = + test_context.execute_and_apply_transaction(Transaction::AutomatedTransaction(automated_txn.clone())); + assert_eq!( + output.status(), + &TransactionStatus::Keep(ExecutionStatus::Success), + "{output:?}" + ); + let dest_account_balance = test_context.account_balance(dest_account.address().clone()); + assert_eq!(dest_account_balance, 1_000_100); + // check that sequence number is not updated. + assert_eq!(sender_seq_num, test_context.account_sequence_number(sender_address)); + + // try to submit automated transaction with incorrect sender + let raw_transaction = dest_account + .account() + .transaction() + .payload(payload) + .sequence_number(sequence_number) + .gas_unit_price(gas_price) + .max_gas_amount(max_gas_amount) + .ttl(expiration_time) + .raw(); + + let parent_hash = HashValue::new([42; HashValue::LENGTH]); + let automated_txn = AutomatedTransaction::new(raw_transaction, parent_hash, 1); + let result = + test_context.execute_tagged_transaction(Transaction::AutomatedTransaction(automated_txn.clone())); + AutomationRegistrationTestContext::check_discarded_output( + result, + StatusCode::NO_ACTIVE_AUTOMATED_TASK, + ); +} diff --git a/aptos-move/e2e-testsuite/src/tests/automation_registration.rs b/aptos-move/e2e-testsuite/src/tests/automation_registration.rs index fdb84a222ba55..9024c1ea7afdd 100644 --- a/aptos-move/e2e-testsuite/src/tests/automation_registration.rs +++ b/aptos-move/e2e-testsuite/src/tests/automation_registration.rs @@ -1,4 +1,5 @@ // Copyright (c) 2024 Supra. +// SPDX-License-Identifier: Apache-2.0 use aptos_cached_packages::aptos_framework_sdk_builder; use aptos_language_e2e_tests::account::{Account, AccountData}; @@ -10,24 +11,37 @@ use aptos_types::transaction::{ }; use move_core_types::vm_status::StatusCode; use std::ops::{Deref, DerefMut}; +use move_core_types::account_address::AccountAddress; const TIMESTAMP_NOW_SECONDS: &str = "0x1::timestamp::now_seconds"; +const ACCOUNT_BALANCE: &str = "0x1::coin::balance"; +const SUPRA_COIN: &str = "0x1::supra_coin::SupraCoin"; +const ACCOUNT_SEQ_NUM: &str = "0x1::account::get_sequence_number"; const AUTOMATION_NEXT_TASK_ID: &str = "0x1::automation_registry::get_next_task_index"; -struct AutomationRegistrationTestContext { +pub(crate) struct AutomationRegistrationTestContext { executor: FakeExecutor, txn_sender: AccountData, } impl AutomationRegistrationTestContext { - fn new() -> Self { + pub(crate) fn sender_account_data(&self) -> &AccountData { + &self.txn_sender + } + pub(crate) fn sender_account_address(&self) -> AccountAddress { + *self.txn_sender.address() + } +} + +impl AutomationRegistrationTestContext { + pub(crate) fn new() -> Self { let mut executor = FakeExecutor::from_head_genesis(); let mut root = Account::new_aptos_root(); let (private_key, public_key) = aptos_vm_genesis::GENESIS_KEYPAIR.clone(); root.rotate_key(private_key, public_key); // Prepare automation registration transaction sender - let txn_sender = executor.create_raw_account_data(100_000_000, 0); + let txn_sender = executor.create_raw_account_data(100_000_000_000, 0); executor.add_account_data(&txn_sender); Self { executor, @@ -35,13 +49,13 @@ impl AutomationRegistrationTestContext { } } - fn new_account_data(&mut self, amount: u64, seq_num: u64) -> AccountData { + pub(crate) fn new_account_data(&mut self, amount: u64, seq_num: u64) -> AccountData { let new_account_data = self.create_raw_account_data(amount, seq_num); self.add_account_data(&new_account_data); new_account_data } - fn create_automation_txn( + pub(crate) fn create_automation_txn( &self, seq_num: u64, inner_payload: EntryFunction, @@ -57,10 +71,11 @@ impl AutomationRegistrationTestContext { .transaction() .payload(automation_txn) .sequence_number(seq_num) + .gas_unit_price(1) .sign() } - fn check_miscellaneous_output(output: TransactionOutput, expected_status_code: StatusCode) { + pub(crate) fn check_miscellaneous_output(output: TransactionOutput, expected_status_code: StatusCode) { match output.status() { TransactionStatus::Keep(ExecutionStatus::MiscellaneousError(maybe_status_code)) => { assert_eq!( @@ -72,6 +87,57 @@ impl AutomationRegistrationTestContext { _ => panic!("Unexpected transaction status: {output:?}"), } } + pub(crate) fn check_discarded_output(output: TransactionOutput, expected_status_code: StatusCode) { + match output.status() { + TransactionStatus::Discard(status_code ) => { + assert_eq!( + status_code, + &expected_status_code, + "{output:?}" + ); + }, + _ => panic!("Unexpected transaction status: {output:?}"), + } + } + + pub (crate) fn chain_time_now(&mut self) -> u64 { + let view_output = self.execute_view_function( + str::parse(TIMESTAMP_NOW_SECONDS).unwrap(), + vec![], + vec![], + ); + let result = view_output.values.expect("Valid result"); + assert_eq!(result.len(), 1); + bcs::from_bytes::(&result[0]).unwrap() + } + + pub(crate) fn advance_chain_time_in_secs(&mut self, secs: u64) { + self.set_block_time(secs * 1_000_000); + self.new_block() + } + + pub(crate) fn account_balance(&mut self, account_address: AccountAddress) -> u64 { + let view_output = self.execute_view_function( + str::parse(ACCOUNT_BALANCE).unwrap(), + vec![str::parse(SUPRA_COIN).unwrap()], + vec![account_address.to_vec()], + ); + let result = view_output.values.expect("Valid result"); + assert_eq!(result.len(), 1); + bcs::from_bytes::(&result[0]).unwrap() + + } + pub(crate) fn account_sequence_number(&mut self, account_address: AccountAddress) -> u64 { + let view_output = self.execute_view_function( + str::parse(ACCOUNT_SEQ_NUM).unwrap(), + vec![], + vec![account_address.to_vec()], + ); + let result = view_output.values.expect("Valid result"); + assert_eq!(result.len(), 1); + bcs::from_bytes::(&result[0]).unwrap() + + } } impl Deref for AutomationRegistrationTestContext { @@ -97,15 +163,7 @@ fn check_successful_registration() { aptos_framework_sdk_builder::supra_coin_mint(dest_account.address().clone(), 100) .into_entry_function(); - let view_output = test_context.execute_view_function( - str::parse(TIMESTAMP_NOW_SECONDS).unwrap(), - vec![], - vec![], - ); - let result = view_output.values.expect("Valid result"); - assert_eq!(result.len(), 1); - let now = bcs::from_bytes::(&result[0]).unwrap(); - let expiration_time = now + 4000; + let expiration_time = test_context.chain_time_now() + 4000; let automation_txn = test_context.create_automation_txn( 0, inner_entry_function.clone(), @@ -114,6 +172,8 @@ fn check_successful_registration() { 100, ); + let sender_address = test_context.sender_account_address(); + let sender_seq_num_old = test_context.account_sequence_number(sender_address); let output = test_context.execute_and_apply(automation_txn); assert_eq!( output.status(), @@ -131,6 +191,8 @@ fn check_successful_registration() { assert_eq!(result.len(), 1); let next_task_id = bcs::from_bytes::(&result[0]).unwrap(); assert_eq!(next_task_id, 1); + let sender_seq_num = test_context.account_sequence_number(sender_address); + assert_eq!(sender_seq_num, sender_seq_num_old + 1); } #[test] diff --git a/aptos-move/e2e-testsuite/src/tests/mod.rs b/aptos-move/e2e-testsuite/src/tests/mod.rs index cfe2007f1b213..cc9674cade9a7 100644 --- a/aptos-move/e2e-testsuite/src/tests/mod.rs +++ b/aptos-move/e2e-testsuite/src/tests/mod.rs @@ -27,3 +27,4 @@ mod scripts; mod transaction_fuzzer; mod verify_txn; mod automation_registration; +mod automated_transactions; diff --git a/aptos-move/framework/supra-framework/doc/automation_registry.md b/aptos-move/framework/supra-framework/doc/automation_registry.md index cb200a29a35c1..f88c0b0317ca5 100644 --- a/aptos-move/framework/supra-framework/doc/automation_registry.md +++ b/aptos-move/framework/supra-framework/doc/automation_registry.md @@ -26,7 +26,7 @@ This contract is part of the Supra Framework and is designed to manage automated - [Function `get_next_task_index`](#0x1_automation_registry_get_next_task_index) - [Function `get_active_task_ids`](#0x1_automation_registry_get_active_task_ids) - [Function `get_task_details`](#0x1_automation_registry_get_task_details) -- [Function `has_active_task_with_id`](#0x1_automation_registry_has_active_task_with_id) +- [Function `has_sender_active_task_with_id`](#0x1_automation_registry_has_sender_active_task_with_id) - [Function `get_registry_fee_address`](#0x1_automation_registry_get_registry_fee_address) - [Function `get_gas_committed_for_next_epoch`](#0x1_automation_registry_get_gas_committed_for_next_epoch) @@ -702,15 +702,15 @@ Error will be returned if entry with specified ID does not exist. - + -## Function `has_active_task_with_id` +## Function `has_sender_active_task_with_id` Checks whether there is an active task in registry with specified input task id.
#[view]
-public fun has_active_task_with_id(id: u64): bool
+public fun has_sender_active_task_with_id(sender: address, id: u64): bool
 
@@ -719,8 +719,8 @@ Checks whether there is an active task in registry with specified input task id. Implementation -
public fun has_active_task_with_id(id: u64): bool {
-    automation_registry_state::has_active_task_with_id(id)
+
public fun has_sender_active_task_with_id(sender:address, id: u64): bool {
+    automation_registry_state::has_sender_active_task_with_id(sender, id)
 }
 
diff --git a/aptos-move/framework/supra-framework/doc/automation_registry_state.md b/aptos-move/framework/supra-framework/doc/automation_registry_state.md index e5556baaa7fba..9731849117b6f 100644 --- a/aptos-move/framework/supra-framework/doc/automation_registry_state.md +++ b/aptos-move/framework/supra-framework/doc/automation_registry_state.md @@ -21,7 +21,7 @@ This contract is part of the Supra Framework and is designed to manage automated - [Function `update_automation_gas_limit`](#0x1_automation_registry_state_update_automation_gas_limit) - [Function `get_active_task_ids`](#0x1_automation_registry_state_get_active_task_ids) - [Function `get_task_details`](#0x1_automation_registry_state_get_task_details) -- [Function `has_active_task_with_id`](#0x1_automation_registry_state_has_active_task_with_id) +- [Function `has_sender_active_task_with_id`](#0x1_automation_registry_state_has_sender_active_task_with_id) - [Function `get_next_task_index`](#0x1_automation_registry_state_get_next_task_index) - [Function `get_gas_committed_for_next_epoch`](#0x1_automation_registry_state_get_gas_committed_for_next_epoch) @@ -687,14 +687,14 @@ Error will be returned if entry with specified ID does not exist. - + -## Function `has_active_task_with_id` +## Function `has_sender_active_task_with_id` -Checks whether there is an active task in registry with specified input task id. +Checks whether there is an active task in registry with specified input task id for the sender exists. -
public(friend) fun has_active_task_with_id(id: u64): bool
+
public(friend) fun has_sender_active_task_with_id(sender: address, id: u64): bool
 
@@ -703,12 +703,12 @@ Checks whether there is an active task in registry with specified input task id. Implementation -
public(friend) fun has_active_task_with_id(id: u64): bool acquires AutomationRegistryState {
+
public(friend) fun has_sender_active_task_with_id(sender: address, id: u64): bool acquires AutomationRegistryState {
     let automation_task_metadata = borrow_global<AutomationRegistryState>(@supra_framework);
     if (enumerable_map::contains(&automation_task_metadata.tasks, id)) {
         let value = enumerable_map::get_value_ref(&automation_task_metadata.tasks, id);
-        value.state != PENDING
-    } else {
+        value.state != PENDING && value.owner == sender
+    } else  {
         false
     }
 }
diff --git a/aptos-move/framework/supra-framework/doc/transaction_validation.md b/aptos-move/framework/supra-framework/doc/transaction_validation.md
index 5aa36bc15fedf..ad20be314d728 100644
--- a/aptos-move/framework/supra-framework/doc/transaction_validation.md
+++ b/aptos-move/framework/supra-framework/doc/transaction_validation.md
@@ -10,10 +10,13 @@
 -  [Function `initialize`](#0x1_transaction_validation_initialize)
 -  [Function `prologue_common`](#0x1_transaction_validation_prologue_common)
 -  [Function `script_prologue`](#0x1_transaction_validation_script_prologue)
+-  [Function `automated_transaction_prologue`](#0x1_transaction_validation_automated_transaction_prologue)
 -  [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 `epilogue`](#0x1_transaction_validation_epilogue)
+-  [Function `automated_transaction_epilogue`](#0x1_transaction_validation_automated_transaction_epilogue)
+-  [Function `epilogue_gas_payer_only`](#0x1_transaction_validation_epilogue_gas_payer_only)
 -  [Function `epilogue_gas_payer`](#0x1_transaction_validation_epilogue_gas_payer)
 -  [Specification](#@Specification_1)
     -  [High-level Requirements](#high-level-req)
@@ -29,6 +32,7 @@
 
 
 
use 0x1::account;
+use 0x1::automation_registry;
 use 0x1::bcs;
 use 0x1::chain_id;
 use 0x1::coin;
@@ -176,6 +180,15 @@ important to the semantics of the system.
 
 
 
+
+
+
+
+
const PROLOGUE_ENO_ACTIVE_AUTOMATED_TASK: u64 = 1012;
+
+ + + @@ -398,6 +411,60 @@ Only called during genesis to initialize system resources for this module. + + + + +## Function `automated_transaction_prologue` + + + +
fun automated_transaction_prologue(sender: signer, task_index: u64, txn_gas_price: u64, txn_max_gas_units: u64, txn_expiration_time: u64, chain_id: u8)
+
+ + + +
+Implementation + + +
fun automated_transaction_prologue(
+    sender: signer,
+    task_index: u64,
+    txn_gas_price: u64,
+    txn_max_gas_units: u64,
+    txn_expiration_time: u64,
+    chain_id: u8,
+)  {
+    let gas_payer = signer::address_of(&sender);
+
+    assert!(chain_id::get() == chain_id, error::invalid_argument(PROLOGUE_EBAD_CHAIN_ID));
+
+    assert!(
+        timestamp::now_seconds() < txn_expiration_time,
+        error::invalid_argument(PROLOGUE_ETRANSACTION_EXPIRED),
+    );
+
+    let max_transaction_fee = txn_gas_price * txn_max_gas_units;
+
+    if (features::operations_default_to_fa_supra_store_enabled()) {
+        assert!(
+            supra_account::is_fungible_balance_at_least(gas_payer, max_transaction_fee),
+            error::invalid_argument(PROLOGUE_ECANT_PAY_GAS_DEPOSIT)
+        );
+    } else {
+        assert!(
+            coin::is_balance_at_least<SupraCoin>(gas_payer, max_transaction_fee),
+            error::invalid_argument(PROLOGUE_ECANT_PAY_GAS_DEPOSIT)
+        );
+    };
+    assert!(automation_registry::has_sender_active_task_with_id(address_of(&sender), task_index),
+        error::invalid_state(PROLOGUE_ENO_ACTIVE_AUTOMATED_TASK))
+}
+
+ + +
@@ -582,15 +649,15 @@ Called by the Adapter - + -## Function `epilogue_gas_payer` +## Function `automated_transaction_epilogue` -Epilogue function with explicit gas payer specified, is run after a transaction is successfully executed. +Epilogue function is run after a automated transaction is successfully executed. Called by the Adapter -
fun epilogue_gas_payer(account: signer, gas_payer: address, storage_fee_refunded: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
+
fun automated_transaction_epilogue(account: signer, storage_fee_refunded: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
 
@@ -599,8 +666,41 @@ Called by the Adapter Implementation -
fun epilogue_gas_payer(
+
fun automated_transaction_epilogue(
     account: signer,
+    storage_fee_refunded: u64,
+    txn_gas_price: u64,
+    txn_max_gas_units: u64,
+    gas_units_remaining: u64
+) {
+    let addr = signer::address_of(&account);
+    epilogue_gas_payer_only(addr, storage_fee_refunded, txn_gas_price, txn_max_gas_units, gas_units_remaining);
+}
+
+ + + + + + + +## Function `epilogue_gas_payer_only` + +Epilogue function with explicit gas payer specified, is run after a transaction is successfully executed. +Called by the Adapter. +Only burns spent gas does not increment sequcence number of the sender account. + + +
fun epilogue_gas_payer_only(gas_payer: address, storage_fee_refunded: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
+
+ + + +
+Implementation + + +
fun epilogue_gas_payer_only(
     gas_payer: address,
     storage_fee_refunded: u64,
     txn_gas_price: u64,
@@ -653,6 +753,40 @@ Called by the Adapter
         transaction_fee::mint_and_refund(gas_payer, mint_amount)
     };
 
+}
+
+ + + +
+ + + +## Function `epilogue_gas_payer` + +Epilogue function with explicit gas payer specified, is run after a transaction is successfully executed. +Called by the Adapter + + +
fun epilogue_gas_payer(account: signer, gas_payer: address, storage_fee_refunded: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
+
+ + + +
+Implementation + + +
fun epilogue_gas_payer(
+    account: signer,
+    gas_payer: address,
+    storage_fee_refunded: u64,
+    txn_gas_price: u64,
+    txn_max_gas_units: u64,
+    gas_units_remaining: u64
+) {
+    epilogue_gas_payer_only(gas_payer, storage_fee_refunded, txn_gas_price, txn_max_gas_units, gas_units_remaining);
+
     // Increment sequence number
     let addr = signer::address_of(&account);
     account::increment_sequence_number(addr);
diff --git a/aptos-move/framework/supra-framework/sources/automation_registry.move b/aptos-move/framework/supra-framework/sources/automation_registry.move
index 518133395ac23..357cb0648479f 100644
--- a/aptos-move/framework/supra-framework/sources/automation_registry.move
+++ b/aptos-move/framework/supra-framework/sources/automation_registry.move
@@ -242,8 +242,8 @@ module supra_framework::automation_registry {
 
     #[view]
     /// Checks whether there is an active task in registry with specified input task id.
-    public fun has_active_task_with_id(id: u64): bool {
-        automation_registry_state::has_active_task_with_id(id)
+    public fun has_sender_active_task_with_id(sender:address, id: u64): bool {
+        automation_registry_state::has_sender_active_task_with_id(sender, id)
     }
 
     #[view]
diff --git a/aptos-move/framework/supra-framework/sources/automation_registry_state.move b/aptos-move/framework/supra-framework/sources/automation_registry_state.move
index 1a86c6bf38c21..b8d42bb64e156 100644
--- a/aptos-move/framework/supra-framework/sources/automation_registry_state.move
+++ b/aptos-move/framework/supra-framework/sources/automation_registry_state.move
@@ -248,13 +248,13 @@ module supra_framework::automation_registry_state {
         enumerable_map::get_value(&automation_task_metadata.tasks, id)
     }
 
-    /// Checks whether there is an active task in registry with specified input task id.
-    public(friend) fun has_active_task_with_id(id: u64): bool acquires AutomationRegistryState {
+    /// Checks whether there is an active task in registry with specified input task id for the sender exists.
+    public(friend) fun has_sender_active_task_with_id(sender: address, id: u64): bool acquires AutomationRegistryState {
         let automation_task_metadata = borrow_global(@supra_framework);
         if (enumerable_map::contains(&automation_task_metadata.tasks, id)) {
             let value = enumerable_map::get_value_ref(&automation_task_metadata.tasks, id);
-            value.state != PENDING
-        } else {
+            value.state != PENDING && value.owner == sender
+        } else  {
             false
         }
     }
diff --git a/aptos-move/framework/supra-framework/sources/transaction_validation.move b/aptos-move/framework/supra-framework/sources/transaction_validation.move
index a8aa5efc1a01b..9a427e5c36e2e 100644
--- a/aptos-move/framework/supra-framework/sources/transaction_validation.move
+++ b/aptos-move/framework/supra-framework/sources/transaction_validation.move
@@ -3,7 +3,9 @@ module supra_framework::transaction_validation {
     use std::error;
     use std::features;
     use std::signer;
+    use std::signer::address_of;
     use std::vector;
+    use supra_framework::automation_registry;
 
     use supra_framework::account;
     use supra_framework::supra_account;
@@ -47,6 +49,8 @@ module supra_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;
+    // It seems 1011 is/was reserved.
+    const PROLOGUE_ENO_ACTIVE_AUTOMATED_TASK: u64 = 1012;
 
     /// Only called during genesis to initialize system resources for this module.
     public(friend) fun initialize(
@@ -167,6 +171,40 @@ module supra_framework::transaction_validation {
         )
     }
 
+    fun automated_transaction_prologue(
+        sender: signer,
+        task_index: u64,
+        txn_gas_price: u64,
+        txn_max_gas_units: u64,
+        txn_expiration_time: u64,
+        chain_id: u8,
+    )  {
+        let gas_payer = signer::address_of(&sender);
+
+        assert!(chain_id::get() == chain_id, error::invalid_argument(PROLOGUE_EBAD_CHAIN_ID));
+
+        assert!(
+            timestamp::now_seconds() < txn_expiration_time,
+            error::invalid_argument(PROLOGUE_ETRANSACTION_EXPIRED),
+        );
+
+        let max_transaction_fee = txn_gas_price * txn_max_gas_units;
+
+        if (features::operations_default_to_fa_supra_store_enabled()) {
+            assert!(
+                supra_account::is_fungible_balance_at_least(gas_payer, max_transaction_fee),
+                error::invalid_argument(PROLOGUE_ECANT_PAY_GAS_DEPOSIT)
+            );
+        } else {
+            assert!(
+                coin::is_balance_at_least(gas_payer, max_transaction_fee),
+                error::invalid_argument(PROLOGUE_ECANT_PAY_GAS_DEPOSIT)
+            );
+        };
+        assert!(automation_registry::has_sender_active_task_with_id(address_of(&sender), task_index),
+            error::invalid_state(PROLOGUE_ENO_ACTIVE_AUTOMATED_TASK))
+    }
+
     fun multi_agent_script_prologue(
         sender: signer,
         txn_sequence_number: u64,
@@ -269,10 +307,23 @@ module supra_framework::transaction_validation {
         epilogue_gas_payer(account, addr, 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.
+    /// Epilogue function is run after a automated transaction is successfully executed.
     /// Called by the Adapter
-    fun epilogue_gas_payer(
+    fun automated_transaction_epilogue(
         account: signer,
+        storage_fee_refunded: u64,
+        txn_gas_price: u64,
+        txn_max_gas_units: u64,
+        gas_units_remaining: u64
+    ) {
+        let addr = signer::address_of(&account);
+        epilogue_gas_payer_only(addr, 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.
+    /// Only burns spent gas does not increment sequcence number of the sender account.
+    fun epilogue_gas_payer_only(
         gas_payer: address,
         storage_fee_refunded: u64,
         txn_gas_price: u64,
@@ -325,6 +376,20 @@ module supra_framework::transaction_validation {
             transaction_fee::mint_and_refund(gas_payer, mint_amount)
         };
 
+    }
+
+    /// Epilogue function with explicit gas payer specified, is run after a transaction is successfully executed.
+    /// Called by the Adapter
+    fun epilogue_gas_payer(
+        account: signer,
+        gas_payer: address,
+        storage_fee_refunded: u64,
+        txn_gas_price: u64,
+        txn_max_gas_units: u64,
+        gas_units_remaining: u64
+    ) {
+        epilogue_gas_payer_only(gas_payer, storage_fee_refunded, txn_gas_price, txn_max_gas_units, gas_units_remaining);
+
         // Increment sequence number
         let addr = signer::address_of(&account);
         account::increment_sequence_number(addr);
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 68967e80834cd..111d0596ed7ed 100644
--- a/third_party/move/move-core/types/src/vm_status.rs
+++ b/third_party/move/move-core/types/src/vm_status.rs
@@ -593,6 +593,8 @@ pub enum StatusCode {
     RESERVED_VALIDATION_ERROR_7 = 42,
     RESERVED_VALIDATION_ERROR_8 = 43,
     RESERVED_VALIDATION_ERROR_9 = 44,
+    // Failed to identify active automated task by provided index/sequence-number
+    NO_ACTIVE_AUTOMATED_TASK = 45,
 
     // When a code module/script is published it is verified. These are the
     // possible errors that can arise from the verification process.
@@ -732,6 +734,8 @@ pub enum StatusCode {
     // Verification errors related to automation registration transaction
     // Validation of the entry function to be automated failed.
     INVALID_AUTOMATION_INNER_PAYLOAD = 1132,
+    // Automated transaction validation failures
+    INVALID_AUTOMATED_PAYLOAD = 1133,
 
 
     // These are errors that the VM might raise if a violation of internal