Skip to content

Commit

Permalink
[AN-Issue-1346] Implemented custom handling of automation transactions
Browse files Browse the repository at this point in the history
- Added separate flow for automation transaction handling. It differes
from user transaction flow only by custom validation step of
inner-payload of automation transaction

- Added validation logic for the automation transaction inner-payload

- Introuced specific StatusCode for failures during automation
transaction inner-payload validation. They are qualified as verification
status type and will lead to have transaction kept and the account will
be charged.
  • Loading branch information
Aregnaz Harutyunyan committed Dec 3, 2024
1 parent 77cac8c commit 6bc98a0
Show file tree
Hide file tree
Showing 16 changed files with 356 additions and 22 deletions.
11 changes: 9 additions & 2 deletions api/src/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use aptos_api_types::{
MAX_RECURSIVE_TYPES_ALLOWED, U64,
};
use aptos_crypto::{hash::CryptoHash, signing_message};
use aptos_types::transaction::automation::AutomationTransactionPayload;
use aptos_types::{
account_address::AccountAddress,
mempool_status::MempoolStatusCode,
Expand Down Expand Up @@ -1031,7 +1032,10 @@ impl TransactionsApi {
})?;
// Verify the signed transaction
match signed_transaction.payload() {
TransactionPayload::EntryFunction(entry_function) => {
TransactionPayload::Automation(
AutomationTransactionPayload::EntryFunction(entry_function),
)
| TransactionPayload::EntryFunction(entry_function) => {
TransactionsApi::validate_entry_function_payload_format(
ledger_info,
entry_function,
Expand Down Expand Up @@ -1380,7 +1384,10 @@ impl TransactionsApi {
format!("Script::{}", txn.committed_hash()).to_string()
},
TransactionPayload::ModuleBundle(_) => "ModuleBundle::unknown".to_string(),
TransactionPayload::EntryFunction(entry_function) => FunctionStats::function_to_key(
TransactionPayload::Automation(AutomationTransactionPayload::EntryFunction(
entry_function,
))
| TransactionPayload::EntryFunction(entry_function) => FunctionStats::function_to_key(
entry_function.module(),
&entry_function.function().into(),
),
Expand Down
7 changes: 4 additions & 3 deletions api/types/src/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use aptos_crypto::{hash::CryptoHash, HashValue};
use aptos_logger::{sample, sample::SampleRate};
use aptos_resource_viewer::AptosValueAnnotator;
use aptos_storage_interface::DbReader;
use aptos_types::transaction::automation::AutomationTransactionPayload;
use aptos_types::{
access_path::{AccessPath, Path},
chain_id::ChainId,
Expand Down Expand Up @@ -177,8 +178,8 @@ impl<'a, S: StateView> MoveConverter<'a, S> {
data: TransactionOnChainData,
) -> Result<Transaction> {
use aptos_types::transaction::Transaction::{
BlockEpilogue, BlockMetadata, BlockMetadataExt, GenesisTransaction, StateCheckpoint,
UserTransaction, AutomatedTransaction
AutomatedTransaction, BlockEpilogue, BlockMetadata, BlockMetadataExt,
GenesisTransaction, StateCheckpoint, UserTransaction,
};
let aux_data = self
.db
Expand Down Expand Up @@ -277,7 +278,7 @@ impl<'a, S: StateView> MoveConverter<'a, S> {
use aptos_types::transaction::TransactionPayload::*;
let ret = match payload {
Script(s) => TransactionPayload::ScriptPayload(s.try_into()?),
EntryFunction(fun) => {
Automation(AutomationTransactionPayload::EntryFunction(fun)) | EntryFunction(fun) => {
let (module, function, ty_args, args) = fun.into_inner();
let func_args = self
.inner
Expand Down
2 changes: 2 additions & 0 deletions aptos-move/aptos-debugger/src/aptos_debugger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use anyhow::{bail, format_err, Result};
use aptos_gas_profiling::{GasProfiler, TransactionGasLog};
use aptos_rest_client::Client;
use aptos_types::transaction::automation::AutomationTransactionPayload;
use aptos_types::{
account_address::AccountAddress,
state_store::TStateView,
Expand Down Expand Up @@ -99,6 +100,7 @@ impl AptosDebugger {
|gas_meter| {
let gas_profiler = match txn.payload() {
TransactionPayload::Script(_) => GasProfiler::new_script(gas_meter),
TransactionPayload::Automation(AutomationTransactionPayload::EntryFunction(entry_func)) |
TransactionPayload::EntryFunction(entry_func) => GasProfiler::new_function(
gas_meter,
entry_func.module().clone(),
Expand Down
162 changes: 154 additions & 8 deletions aptos-move/aptos-vm/src/aptos_vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use aptos_logger::{enabled, prelude::*, Level};
use aptos_metrics_core::TimerHelper;
#[cfg(any(test, feature = "testing"))]
use aptos_types::state_store::StateViewId;
use aptos_types::transaction::automation::AutomationTransactionPayload;
use aptos_types::{
account_config::{self, new_block_event_key, AccountResource},
block_executor::{
Expand Down Expand Up @@ -835,6 +836,7 @@ impl AptosVM {
},
TransactionPayload::EntryFunction(entry_fn) => {
session.execute(|session| {
// TODO To enclosed payload validation as well before actually executing the entry function
self.validate_and_execute_entry_function(
resolver,
session,
Expand Down Expand Up @@ -878,6 +880,87 @@ impl AptosVM {
traversal_context,
)
}
fn execute_automation_transaction<'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,
payload: &'a AutomationTransactionPayload,
log_context: &AdapterLogSchema,
new_published_modules_loaded: &mut bool,
change_set_configs: &ChangeSetConfigs,
) -> Result<(VMStatus, VMOutput), VMStatus> {
fail_point!("aptos_vm::execute_automation_transaction", |_| {
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,
})
});
if !payload.is_valid() {
return Err(VMStatus::error(
StatusCode::INVALID_AUTOMATION_PAYLOAD,
Some(AutomationTransactionPayload::entry_function_reference()),
));
}

gas_meter.charge_intrinsic_gas_for_transaction(txn_data.transaction_size())?;
if txn_data.is_keyless() {
gas_meter.charge_keyless()?;
}

match payload {
AutomationTransactionPayload::EntryFunction(entry_fn) => {
session.execute(|session| {
self.validate_automation_txn_inner_payload(
session,
gas_meter,
txn_data.senders(),
entry_fn,
)
})?;
session.execute(|session| {
self.validate_and_execute_entry_function(
resolver,
session,
gas_meter,
traversal_context,
txn_data.senders(),
entry_fn,
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,
)
}

fn charge_change_set(
&self,
Expand Down Expand Up @@ -1742,6 +1825,18 @@ impl AptosVM {
TransactionPayload::ModuleBundle(_) => {
unwrap_or_discard!(Err(deprecated_module_bundle!()))
},
TransactionPayload::Automation(automation_payload) => self
.execute_automation_transaction(
resolver,
user_session,
gas_meter,
&mut traversal_context,
&txn_data,
automation_payload,
log_context,
&mut new_published_modules_loaded,
change_set_configs,
),
};

let gas_usage = txn_data
Expand Down Expand Up @@ -2258,14 +2353,14 @@ impl AptosVM {
)?;

match payload {
TransactionPayload::Script(_) | TransactionPayload::EntryFunction(_) => {
transaction_validation::run_script_prologue(
session,
txn_data,
log_context,
traversal_context,
)
},
TransactionPayload::Script(_)
| TransactionPayload::EntryFunction(_)
| TransactionPayload::Automation(_) => transaction_validation::run_script_prologue(
session,
txn_data,
log_context,
traversal_context,
),
TransactionPayload::Multisig(multisig_payload) => {
// Still run script prologue for multisig transaction to ensure the same tx
// validations are still run for this multisig execution tx, which is submitted by
Expand Down Expand Up @@ -2441,6 +2536,57 @@ impl AptosVM {
},
})
}

/// Validates inner payload/entry function of automation transaction to be valid.
/// It is expected that the first actual argument of the automation transaction payload is
/// the bcs serialized payload/entry function representing automation task.
fn validate_automation_txn_inner_payload(
&self,
session: &mut SessionExt,
_gas_meter: &mut impl AptosGasMeter,
senders: Vec<AccountAddress>,
automation_entry_fn: &EntryFunction,
) -> Result<(), VMStatus> {
if automation_entry_fn.args().is_empty() {
return Err(VMStatus::error(
StatusCode::INVALID_AUTOMATION_PAYLOAD_ARGUMENTS,
Some("Automation transaction payload arguments are empty".to_string()),
));
}
let maybe_inner_payload_bytes = &automation_entry_fn.args()[0];
let inner_entry_function = bcs::from_bytes::<EntryFunction>(maybe_inner_payload_bytes).map_err(|e| {
VMStatus::error(StatusCode::FAILED_TO_DESERIALIZE_ARGUMENT,
Some(format!("Automation transaction payload first argument is expected to be BCS bytes of entry-function {}", e)))
})?;

let function = session.load_function(
inner_entry_function.module(),
inner_entry_function.function(),
inner_entry_function.ty_args(),
)?;
let struct_constructors_enabled =
self.features().is_enabled(FeatureFlag::STRUCT_CONSTRUCTORS);
let (_, _, _, actual_args) = inner_entry_function.into_inner();
// By constructing args we are making sure that function execution in scope of automated
// task will not fail due to invalid arguments passed
verifier::transaction_arg_validation::validate_combine_signer_and_txn_args(
session,
senders,
actual_args,
&function,
struct_constructors_enabled,
)
.map_err(|e| {
VMStatus::error(
StatusCode::INVALID_AUTOMATION_INNER_PAYLOAD,
Some(format!(
"Automation transaction inner payload validation failed. Details: {:?}",
e
)),
)
})
.map(drop)
}
}

// Executor external API
Expand Down
11 changes: 11 additions & 0 deletions aptos-move/aptos-vm/src/transaction_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use aptos_crypto::HashValue;
use aptos_gas_algebra::{FeePerGasUnit, Gas, NumBytes};
use aptos_types::transaction::automated_transaction::AutomatedTransaction;
use aptos_types::transaction::automation::AutomationTransactionPayload;
use aptos_types::{
account_address::AccountAddress,
chain_id::ChainId,
Expand Down Expand Up @@ -34,6 +35,7 @@ pub struct TransactionMetadata {
pub is_keyless: bool,
pub entry_function_payload: Option<EntryFunction>,
pub multisig_payload: Option<Multisig>,
pub automation_payload: Option<AutomationTransactionPayload>,
pub txn_app_hash: Vec<u8>,
}

Expand Down Expand Up @@ -68,6 +70,7 @@ impl TransactionMetadata {
// Deprecated. Return an empty vec because we cannot do anything
// else here, only `unreachable!` otherwise.
TransactionPayload::ModuleBundle(_) => vec![],
TransactionPayload::Automation(_) => vec![],
},
script_size: match txn.payload() {
TransactionPayload::Script(s) => (s.code().len() as u64).into(),
Expand All @@ -84,6 +87,10 @@ impl TransactionMetadata {
TransactionPayload::Multisig(m) => Some(m.clone()),
_ => None,
},
automation_payload: match txn.payload() {
TransactionPayload::Automation(a) => Some(a.clone()),
_ => None,
},
txn_app_hash: HashValue::sha3_256_of(
&bcs::to_bytes(&txn).expect("Unable to serialize SignedTransaction"),
)
Expand Down Expand Up @@ -169,6 +176,9 @@ impl TransactionMetadata {
.map(|entry_func| entry_func.as_entry_function_payload()),
self.multisig_payload()
.map(|multisig| multisig.as_multisig_payload()),
self.automation_payload
.as_ref()
.and_then(|e| e.as_entry_function_payload()),
self.txn_app_hash.clone(),
)
}
Expand Down Expand Up @@ -197,6 +207,7 @@ impl From<&AutomatedTransaction> for TransactionMetadata {
_ => None,
},
multisig_payload: None,
automation_payload: None,
txn_app_hash: txn.hash().to_vec(),
}
}
Expand Down
6 changes: 5 additions & 1 deletion aptos-move/e2e-tests/src/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use aptos_gas_meter::{StandardGasAlgebra, StandardGasMeter};
use aptos_gas_profiling::{GasProfiler, TransactionGasLog};
use aptos_gas_schedule::{AptosGasParameters, InitialGasSchedule, LATEST_GAS_FEATURE_VERSION};
use aptos_keygen::KeyGen;
use aptos_types::transaction::automation::AutomationTransactionPayload;
use aptos_types::{
account_config::{
new_block_event_key, AccountResource, CoinInfoResource, CoinStoreResource, NewBlockEvent,
Expand Down Expand Up @@ -684,7 +685,10 @@ impl FakeExecutor {
|gas_meter| {
let gas_profiler = match txn.payload() {
TransactionPayload::Script(_) => GasProfiler::new_script(gas_meter),
TransactionPayload::EntryFunction(entry_func) => GasProfiler::new_function(
TransactionPayload::Automation(
AutomationTransactionPayload::EntryFunction(entry_func),
)
| TransactionPayload::EntryFunction(entry_func) => GasProfiler::new_function(
gas_meter,
entry_func.module().clone(),
entry_func.function().to_owned(),
Expand Down
32 changes: 32 additions & 0 deletions aptos-move/framework/src/natives/transaction_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,34 @@ fn native_multisig_payload_internal(
}
}

fn native_automation_payload_internal(
context: &mut SafeNativeContext,
mut _ty_args: Vec<Type>,
_args: VecDeque<Value>,
) -> SafeNativeResult<SmallVec<[Value; 1]>> {
context.charge(TRANSACTION_CONTEXT_ENTRY_FUNCTION_PAYLOAD_BASE)?;

let user_transaction_context_opt = get_user_transaction_context_opt_from_context(context);

if let Some(transaction_context) = user_transaction_context_opt {
if let Some(entry_function_payload) = transaction_context.automation_payload() {
let num_bytes = num_bytes_from_entry_function_payload(&entry_function_payload);
context.charge(
TRANSACTION_CONTEXT_ENTRY_FUNCTION_PAYLOAD_PER_BYTE_IN_STR
* NumBytes::new(num_bytes as u64),
)?;
let payload = create_entry_function_payload(entry_function_payload);
Ok(smallvec![create_option_some_value(payload)])
} else {
Ok(smallvec![create_option_none()])
}
} else {
Err(SafeNativeError::Abort {
abort_code: error::invalid_state(abort_codes::ETRANSACTION_CONTEXT_NOT_AVAILABLE),
})
}
}

fn get_user_transaction_context_opt_from_context<'a>(
context: &'a SafeNativeContext,
) -> &'a Option<UserTransactionContext> {
Expand Down Expand Up @@ -432,6 +460,10 @@ pub fn make_all(
"multisig_payload_internal",
native_multisig_payload_internal,
),
(
"automation_payload_internal",
native_automation_payload_internal,
),
("txn_app_hash_internal", native_txn_app_hash_internal),
];

Expand Down
Loading

0 comments on commit 6bc98a0

Please sign in to comment.