From 6d4b49514f1fba0a23c2d515f67b7345ec7c1603 Mon Sep 17 00:00:00 2001 From: gerben-stavenga <54682677+gerben-stavenga@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:19:02 -0700 Subject: [PATCH] Aptos gas payer (#8773) * initial commit * Support gas payer * Remove relics * Fix * Make verification know about gas payer bit * update comments * Remove merge conflict * Give MSB a good name * Reformat * improve * add comment * Fix spec --- .../src/components/feature_flags.rs | 3 + aptos-move/aptos-vm/src/aptos_vm.rs | 21 +- aptos-move/aptos-vm/src/aptos_vm_impl.rs | 96 +++-- aptos-move/aptos-vm/src/errors.rs | 6 + .../e2e-move-tests/src/tests/scripts.rs | 346 +++++++++++++++++- .../string_args.data/pack/sources/hello.move | 3 + .../doc/transaction_validation.md | 178 ++++++++- .../sources/transaction_validation.move | 49 ++- .../sources/transaction_validation.spec.move | 98 ++++- .../framework/move-stdlib/doc/features.md | 47 +++ .../move-stdlib/doc/fixed_point32.md | 7 +- .../move-stdlib/sources/configs/features.move | 7 + .../sources/configs/features.spec.move | 4 + .../move-stdlib/sources/fixed_point32.move | 4 +- .../move/move-core/types/src/vm_status.rs | 2 +- .../resources/transaction_validation.rs | 2 + types/src/on_chain_config/aptos_features.rs | 1 + 17 files changed, 786 insertions(+), 88 deletions(-) diff --git a/aptos-move/aptos-release-builder/src/components/feature_flags.rs b/aptos-move/aptos-release-builder/src/components/feature_flags.rs index 6f6506c38c738..de914e7e2e0c6 100644 --- a/aptos-move/aptos-release-builder/src/components/feature_flags.rs +++ b/aptos-move/aptos-release-builder/src/components/feature_flags.rs @@ -40,6 +40,7 @@ pub enum FeatureFlag { StorageSlotMetadata, ChargeInvariantViolation, DelegationPoolPartialGovernanceVoting, + GasPayerEnabled, } fn generate_features_blob(writer: &CodeWriter, data: &[u64]) { @@ -160,6 +161,7 @@ impl From for AptosFeatureFlag { FeatureFlag::DelegationPoolPartialGovernanceVoting => { AptosFeatureFlag::DELEGATION_POOL_PARTIAL_GOVERNANCE_VOTING }, + FeatureFlag::GasPayerEnabled => AptosFeatureFlag::GAS_PAYER_ENABLED, } } } @@ -203,6 +205,7 @@ impl From for FeatureFlag { AptosFeatureFlag::DELEGATION_POOL_PARTIAL_GOVERNANCE_VOTING => { FeatureFlag::DelegationPoolPartialGovernanceVoting }, + AptosFeatureFlag::GAS_PAYER_ENABLED => FeatureFlag::GasPayerEnabled, } } } diff --git a/aptos-move/aptos-vm/src/aptos_vm.rs b/aptos-move/aptos-vm/src/aptos_vm.rs index ee81cbb99c92f..2e59c9c376dda 100644 --- a/aptos-move/aptos-vm/src/aptos_vm.rs +++ b/aptos-move/aptos-vm/src/aptos_vm.rs @@ -6,7 +6,7 @@ use crate::{ adapter_common::{ discard_error_output, discard_error_vm_status, PreprocessedTransaction, VMAdapter, }, - aptos_vm_impl::{get_transaction_output, AptosVMImpl, AptosVMInternals}, + aptos_vm_impl::{get_transaction_output, AptosVMImpl, AptosVMInternals, GAS_PAYER_FLAG_BIT}, block_executor::BlockAptosVM, counters::*, data_cache::StorageAdapter, @@ -394,6 +394,18 @@ impl AptosVM { )?) } + fn get_senders(txn_data: &TransactionMetadata) -> Vec { + let mut res = vec![txn_data.sender]; + res.extend(txn_data.secondary_signers()); + if txn_data.sequence_number & GAS_PAYER_FLAG_BIT != 0 { + // In a gas payer tx, the last multi-agent signer of the secondary signers is in + // fact the gas payer and not to be part of the tx parameters. So we remove the last + // signer. + res.pop(); + } + res + } + fn execute_script_or_entry_function( &self, resolver: &impl MoveResolverExt, @@ -419,8 +431,7 @@ impl AptosVM { match payload { TransactionPayload::Script(script) => { - let mut senders = vec![txn_data.sender()]; - senders.extend(txn_data.secondary_signers()); + let senders = Self::get_senders(txn_data); let loaded_func = session.load_script(script.code(), script.ty_args().to_vec())?; let args = @@ -441,9 +452,7 @@ impl AptosVM { )?; }, TransactionPayload::EntryFunction(script_fn) => { - let mut senders = vec![txn_data.sender()]; - - senders.extend(txn_data.secondary_signers()); + let senders = Self::get_senders(txn_data); self.validate_and_execute_entry_function( &mut session, gas_meter, diff --git a/aptos-move/aptos-vm/src/aptos_vm_impl.rs b/aptos-move/aptos-vm/src/aptos_vm_impl.rs index badf480d0e36e..c5487af174b71 100644 --- a/aptos-move/aptos-vm/src/aptos_vm_impl.rs +++ b/aptos-move/aptos-vm/src/aptos_vm_impl.rs @@ -31,7 +31,10 @@ use aptos_types::{ use aptos_vm_logging::{log_schema::AdapterLogSchema, prelude::*}; use aptos_vm_types::output::VMOutput; use fail::fail_point; -use move_binary_format::{errors::VMResult, CompiledModule}; +use move_binary_format::{ + errors::{Location, PartialVMError, VMResult}, + CompiledModule, +}; use move_core_types::{ gas_algebra::NumArgs, language_storage::ModuleId, @@ -43,6 +46,7 @@ use move_vm_types::gas::UnmeteredGasMeter; use std::sync::Arc; pub const MAXIMUM_APPROVED_TRANSACTION_SIZE: u64 = 1024 * 1024; +pub const GAS_PAYER_FLAG_BIT: u64 = 1u64 << 63; // MSB of sequence number is used to flag a gas payer tx /// A wrapper to make VMRuntime standalone pub struct AptosVMImpl { @@ -502,28 +506,21 @@ impl AptosVMImpl { .or_else(|err| convert_prologue_error(transaction_validation, err, log_context)) } - /// Run the epilogue of a transaction by calling into `EPILOGUE_NAME` function stored - /// in the `ACCOUNT_MODULE` on chain. - pub(crate) fn run_success_epilogue( + fn run_epiloque( &self, session: &mut SessionExt, gas_remaining: Gas, txn_data: &TransactionMetadata, - log_context: &AdapterLogSchema, - ) -> Result<(), VMStatus> { - fail_point!("move_adapter::run_success_epilogue", |_| { - Err(VMStatus::error( - StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, - None, - )) - }); - - let transaction_validation = self.transaction_validation(); + transaction_validation: &TransactionValidation, + ) -> VMResult<()> { let txn_sequence_number = txn_data.sequence_number(); let txn_gas_price = txn_data.gas_unit_price(); let txn_max_gas_units = txn_data.max_gas_amount(); - session - .execute_function_bypass_visibility( + // 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 txn_sequence_number & GAS_PAYER_FLAG_BIT == 0 { + // Regular tx, run the normal epilogue + session.execute_function_bypass_visibility( &transaction_validation.module_id(), &transaction_validation.user_epilogue_name, // TODO: Deprecate this once we remove gas currency on the Move side. @@ -537,8 +534,50 @@ impl AptosVMImpl { ]), &mut UnmeteredGasMeter, ) - .map(|_return_vals| ()) - .map_err(expect_no_verification_errors) + } else { + // Gas payer tx + let gas_payer = *txn_data.secondary_signers.last().ok_or_else(|| { + PartialVMError::new(StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR) + .finish(Location::Undefined) + })?; + session.execute_function_bypass_visibility( + &transaction_validation.module_id(), + &transaction_validation.user_epilogue_gas_payer_name, + // TODO: Deprecate this once we remove gas currency on the Move side. + vec![], + serialize_values(&vec![ + MoveValue::Signer(txn_data.sender), + MoveValue::Address(gas_payer), + MoveValue::U64(txn_sequence_number), + MoveValue::U64(txn_gas_price.into()), + MoveValue::U64(txn_max_gas_units.into()), + MoveValue::U64(gas_remaining.into()), + ]), + &mut UnmeteredGasMeter, + ) + } + .map(|_return_vals| ()) + .map_err(expect_no_verification_errors) + } + + /// Run the epilogue of a transaction by calling into `EPILOGUE_NAME` function stored + /// in the `ACCOUNT_MODULE` on chain. + pub(crate) fn run_success_epilogue( + &self, + session: &mut SessionExt, + gas_remaining: Gas, + txn_data: &TransactionMetadata, + log_context: &AdapterLogSchema, + ) -> Result<(), VMStatus> { + fail_point!("move_adapter::run_success_epilogue", |_| { + Err(VMStatus::error( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + None, + )) + }); + + let transaction_validation = self.transaction_validation(); + self.run_epiloque(session, gas_remaining, txn_data, transaction_validation) .or_else(|err| convert_epilogue_error(transaction_validation, err, log_context)) } @@ -552,26 +591,7 @@ impl AptosVMImpl { log_context: &AdapterLogSchema, ) -> Result<(), VMStatus> { let transaction_validation = self.transaction_validation(); - let txn_sequence_number = txn_data.sequence_number(); - let txn_gas_price = txn_data.gas_unit_price(); - let txn_max_gas_units = txn_data.max_gas_amount(); - session - .execute_function_bypass_visibility( - &transaction_validation.module_id(), - &transaction_validation.user_epilogue_name, - // TODO: Deprecate this once we remove gas currency on the Move side. - vec![], - serialize_values(&vec![ - MoveValue::Signer(txn_data.sender), - MoveValue::U64(txn_sequence_number), - MoveValue::U64(txn_gas_price.into()), - MoveValue::U64(txn_max_gas_units.into()), - MoveValue::U64(gas_remaining.into()), - ]), - &mut UnmeteredGasMeter, - ) - .map(|_return_vals| ()) - .map_err(expect_no_verification_errors) + self.run_epiloque(session, gas_remaining, txn_data, transaction_validation) .or_else(|e| { expect_only_successful_execution( e, diff --git a/aptos-move/aptos-vm/src/errors.rs b/aptos-move/aptos-vm/src/errors.rs index ef3c0a87fdf6a..d3bc1cf90a0fa 100644 --- a/aptos-move/aptos-vm/src/errors.rs +++ b/aptos-move/aptos-vm/src/errors.rs @@ -31,6 +31,9 @@ pub const EBAD_CHAIN_ID: u64 = 1007; pub const ESEQUENCE_NUMBER_TOO_BIG: u64 = 1008; // Counts of secondary keys and addresses don't match. 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; + // 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. @@ -116,6 +119,9 @@ pub fn convert_prologue_error( (INVALID_ARGUMENT, ESECONDARY_KEYS_ADDRESSES_COUNT_MISMATCH) => { StatusCode::SECONDARY_KEYS_ADDRESSES_COUNT_MISMATCH }, + (INVALID_ARGUMENT, EGAS_PAYER_ACCOUNT_MISSING) => { + StatusCode::GAS_PAYER_ACCOUNT_MISSING + }, (category, reason) => { let err_msg = format!("[aptos_vm] Unexpected prologue Move abort: {:?}::{:?} (Category: {:?} Reason: {:?})", location, code, category, reason); diff --git a/aptos-move/e2e-move-tests/src/tests/scripts.rs b/aptos-move/e2e-move-tests/src/tests/scripts.rs index 46826e5c415eb..2e8317250588d 100644 --- a/aptos-move/e2e-move-tests/src/tests/scripts.rs +++ b/aptos-move/e2e-move-tests/src/tests/scripts.rs @@ -2,13 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{assert_success, tests::common, MoveHarness}; -use aptos_language_e2e_tests::account::TransactionBuilder; +use aptos::move_tool::MemberId; +use aptos_language_e2e_tests::{account::TransactionBuilder, transaction_status_eq}; use aptos_types::{ account_address::AccountAddress, account_config::CoinStoreResource, - transaction::{Script, TransactionArgument}, + on_chain_config::FeatureFlag, + transaction::{EntryFunction, ExecutionStatus, Script, TransactionArgument, TransactionStatus}, }; -use move_core_types::move_resource::MoveStructType; +use move_core_types::{move_resource::MoveStructType, vm_status::StatusCode}; #[test] fn test_two_to_two_transfer() { @@ -80,3 +82,341 @@ fn read_coin(h: &MoveHarness, account: &AccountAddress) -> u64 { .unwrap() .coin() } + +#[test] +fn test_two_to_two_transfer_gas_payer() { + let mut h = MoveHarness::new_with_features(vec![FeatureFlag::GAS_PAYER_ENABLED], vec![]); + + let alice = h.new_account_at(AccountAddress::from_hex_literal("0xa11ce").unwrap()); + let bob = h.new_account_at(AccountAddress::from_hex_literal("0xb0b").unwrap()); + let carol = h.new_account_at(AccountAddress::from_hex_literal("0xca501").unwrap()); + let david = h.new_account_at(AccountAddress::from_hex_literal("0xda51d").unwrap()); + let payer = h.new_account_at(AccountAddress::from_hex_literal("0xea51d").unwrap()); + + let amount_alice = 100; + let amount_bob = 200; + let amount_carol = 50; + let amount_david = amount_alice + amount_bob - amount_carol; + + let build_options = aptos_framework::BuildOptions { + with_srcs: false, + with_abis: false, + with_source_maps: false, + with_error_map: false, + ..aptos_framework::BuildOptions::default() + }; + + let package = aptos_framework::BuiltPackage::build( + common::test_dir_path("../../../move-examples/scripts/two_by_two_transfer"), + build_options, + ) + .expect("building package must succeed"); + + let alice_start = read_coin(&h, alice.address()); + let bob_start = read_coin(&h, bob.address()); + let carol_start = read_coin(&h, carol.address()); + let david_start = read_coin(&h, david.address()); + let payer_start = read_coin(&h, payer.address()); + + let code = package.extract_script_code()[0].clone(); + let script = Script::new(code, vec![], vec![ + TransactionArgument::U64(amount_alice), + TransactionArgument::U64(amount_bob), + TransactionArgument::Address(*carol.address()), + TransactionArgument::Address(*david.address()), + TransactionArgument::U64(amount_carol), + ]); + + let transaction = TransactionBuilder::new(alice.clone()) + .secondary_signers(vec![bob.clone(), payer.clone()]) + .script(script) + .sequence_number(h.sequence_number(alice.address()) | (1u64 << 63)) + .max_gas_amount(1_000_000) + .gas_unit_price(1) + .sign_multi_agent(); + + let output = h.executor.execute_transaction(transaction); + assert_success!(output.status().to_owned()); + h.executor.apply_write_set(output.write_set()); + + let alice_end = read_coin(&h, alice.address()); + let bob_end = read_coin(&h, bob.address()); + let carol_end = read_coin(&h, carol.address()); + let david_end = read_coin(&h, david.address()); + let payer_end = read_coin(&h, payer.address()); + + // Make sure sender alice doesn't pay gas + assert_eq!(alice_start - amount_alice, alice_end); + assert_eq!(bob_start - amount_bob, bob_end); + assert_eq!(carol_start + amount_carol, carol_end); + assert_eq!(david_start + amount_david, david_end); + // Make sure payer pays + assert_eq!(payer_start - output.gas_used(), payer_end); +} + +#[test] +fn test_two_to_two_transfer_gas_payer_without_gas_bit_set_fails() { + let mut h = MoveHarness::new_with_features(vec![FeatureFlag::GAS_PAYER_ENABLED], vec![]); + + let alice = h.new_account_at(AccountAddress::from_hex_literal("0xa11ce").unwrap()); + let bob = h.new_account_at(AccountAddress::from_hex_literal("0xb0b").unwrap()); + let carol = h.new_account_at(AccountAddress::from_hex_literal("0xca501").unwrap()); + let david = h.new_account_at(AccountAddress::from_hex_literal("0xda51d").unwrap()); + let payer = h.new_account_at(AccountAddress::from_hex_literal("0xea51d").unwrap()); + + let amount_alice = 100; + let amount_bob = 200; + let amount_carol = 50; + + let build_options = aptos_framework::BuildOptions { + with_srcs: false, + with_abis: false, + with_source_maps: false, + with_error_map: false, + ..aptos_framework::BuildOptions::default() + }; + + let package = aptos_framework::BuiltPackage::build( + common::test_dir_path("../../../move-examples/scripts/two_by_two_transfer"), + build_options, + ) + .expect("building package must succeed"); + + let code = package.extract_script_code()[0].clone(); + let script = Script::new(code, vec![], vec![ + TransactionArgument::U64(amount_alice), + TransactionArgument::U64(amount_bob), + TransactionArgument::Address(*carol.address()), + TransactionArgument::Address(*david.address()), + TransactionArgument::U64(amount_carol), + ]); + + let transaction = TransactionBuilder::new(alice.clone()) + .secondary_signers(vec![bob, payer]) + .script(script) + .sequence_number(h.sequence_number(alice.address())) + .max_gas_amount(1_000_000) + .gas_unit_price(1) + .sign_multi_agent(); + + let output = h.executor.execute_transaction(transaction); + println!("{:?}", output.status()); + assert!(transaction_status_eq( + output.status(), + &TransactionStatus::Keep(ExecutionStatus::MiscellaneousError(Some( + StatusCode::NUMBER_OF_SIGNER_ARGUMENTS_MISMATCH + ))) + )); +} + +#[test] +fn test_two_to_two_transfer_gas_payer_without_feature() { + let mut h = MoveHarness::new(); + + let alice = h.new_account_at(AccountAddress::from_hex_literal("0xa11ce").unwrap()); + let bob = h.new_account_at(AccountAddress::from_hex_literal("0xb0b").unwrap()); + let carol = h.new_account_at(AccountAddress::from_hex_literal("0xca501").unwrap()); + let david = h.new_account_at(AccountAddress::from_hex_literal("0xda51d").unwrap()); + let payer = h.new_account_at(AccountAddress::from_hex_literal("0xea51d").unwrap()); + + let amount_alice = 100; + let amount_bob = 200; + let amount_carol = 50; + + let build_options = aptos_framework::BuildOptions { + with_srcs: false, + with_abis: false, + with_source_maps: false, + with_error_map: false, + ..aptos_framework::BuildOptions::default() + }; + + let package = aptos_framework::BuiltPackage::build( + common::test_dir_path("../../../move-examples/scripts/two_by_two_transfer"), + build_options, + ) + .expect("building package must succeed"); + + let code = package.extract_script_code()[0].clone(); + let script = Script::new(code, vec![], vec![ + TransactionArgument::U64(amount_alice), + TransactionArgument::U64(amount_bob), + TransactionArgument::Address(*carol.address()), + TransactionArgument::Address(*david.address()), + TransactionArgument::U64(amount_carol), + ]); + + let transaction = TransactionBuilder::new(alice.clone()) + .secondary_signers(vec![bob, payer]) + .script(script) + .sequence_number(h.sequence_number(alice.address()) | (1u64 << 63)) + .max_gas_amount(1_000_000) + .gas_unit_price(1) + .sign_multi_agent(); + + let output = h.executor.execute_transaction(transaction); + assert!(transaction_status_eq( + output.status(), + &TransactionStatus::Discard(StatusCode::SEQUENCE_NUMBER_TOO_BIG) + )); +} + +#[test] +fn test_two_to_two_transfer_gas_payer_without_gas_payer() { + let mut h = MoveHarness::new_with_features(vec![FeatureFlag::GAS_PAYER_ENABLED], vec![]); + + let alice = h.new_account_at(AccountAddress::from_hex_literal("0xa11ce").unwrap()); + let bob = h.new_account_at(AccountAddress::from_hex_literal("0xb0b").unwrap()); + let carol = h.new_account_at(AccountAddress::from_hex_literal("0xca501").unwrap()); + let david = h.new_account_at(AccountAddress::from_hex_literal("0xda51d").unwrap()); + + let amount_alice = 100; + let amount_bob = 200; + let amount_carol = 50; + + let build_options = aptos_framework::BuildOptions { + with_srcs: false, + with_abis: false, + with_source_maps: false, + with_error_map: false, + ..aptos_framework::BuildOptions::default() + }; + + let package = aptos_framework::BuiltPackage::build( + common::test_dir_path("../../../move-examples/scripts/two_by_two_transfer"), + build_options, + ) + .expect("building package must succeed"); + + let code = package.extract_script_code()[0].clone(); + let script = Script::new(code, vec![], vec![ + TransactionArgument::U64(amount_alice), + TransactionArgument::U64(amount_bob), + TransactionArgument::Address(*carol.address()), + TransactionArgument::Address(*david.address()), + TransactionArgument::U64(amount_carol), + ]); + + let transaction = TransactionBuilder::new(alice.clone()) + .secondary_signers(vec![bob]) + .script(script) + .sequence_number(h.sequence_number(alice.address()) | (1u64 << 63)) + .max_gas_amount(1_000_000) + .gas_unit_price(1) + .sign_multi_agent(); + + let output = h.executor.execute_transaction(transaction); + // The last signer became the gas payer and thus, the execution errors with a mismatch + // between required signers as parameters and signers passed in. + assert!(transaction_status_eq( + output.status(), + &TransactionStatus::Keep(ExecutionStatus::MiscellaneousError(Some( + StatusCode::NUMBER_OF_SIGNER_ARGUMENTS_MISMATCH + ))) + )); +} + +#[test] +fn test_normal_tx_with_signer_with_gas_payer() { + let mut h = MoveHarness::new_with_features(vec![FeatureFlag::GAS_PAYER_ENABLED], vec![]); + + let alice = h.new_account_at(AccountAddress::from_hex_literal("0xa11ce").unwrap()); + let bob = h.new_account_at(AccountAddress::from_hex_literal("0xb0b").unwrap()); + + let alice_start = read_coin(&h, alice.address()); + let bob_start = read_coin(&h, bob.address()); + + // Load the code + let acc = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); + assert_success!(h.publish_package(&acc, &common::test_dir_path("string_args.data/pack"))); + + let fun: MemberId = str::parse("0xcafe::test::hi").unwrap(); + let entry = EntryFunction::new(fun.module_id, fun.member_id, vec![], vec![bcs::to_bytes( + &"Hi", + ) + .unwrap()]); + let transaction = TransactionBuilder::new(alice.clone()) + .secondary_signers(vec![bob.clone()]) + .entry_function(entry) + .sequence_number(h.sequence_number(alice.address()) | (1u64 << 63)) + .max_gas_amount(1_000_000) + .gas_unit_price(1) + .sign_multi_agent(); + + let output = h.executor.execute_transaction(transaction); + // The last signer became the gas payer and thus, the execution errors with a mismatch + // between required signers as parameters and signers passed in. + assert_success!(output.status().to_owned()); + h.executor.apply_write_set(output.write_set()); + + let alice_after = read_coin(&h, alice.address()); + let bob_after = read_coin(&h, bob.address()); + + assert_eq!(alice_start, alice_after); + assert!(bob_start > bob_after); +} + +#[test] +fn test_normal_tx_without_signer_with_gas_payer() { + let mut h = MoveHarness::new_with_features(vec![FeatureFlag::GAS_PAYER_ENABLED], vec![]); + + let alice = h.new_account_at(AccountAddress::from_hex_literal("0xa11ce").unwrap()); + let bob = h.new_account_at(AccountAddress::from_hex_literal("0xb0b").unwrap()); + + let alice_start = read_coin(&h, alice.address()); + let bob_start = read_coin(&h, bob.address()); + + // Load the code + let acc = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); + assert_success!(h.publish_package(&acc, &common::test_dir_path("string_args.data/pack"))); + + let fun: MemberId = str::parse("0xcafe::test::nothing").unwrap(); + let entry = EntryFunction::new(fun.module_id, fun.member_id, vec![], vec![]); + let transaction = TransactionBuilder::new(alice.clone()) + .secondary_signers(vec![bob.clone()]) + .entry_function(entry) + .sequence_number(h.sequence_number(alice.address()) | (1u64 << 63)) + .max_gas_amount(1_000_000) + .gas_unit_price(1) + .sign_multi_agent(); + + let output = h.executor.execute_transaction(transaction); + // The last signer became the gas payer and thus, the execution errors with a mismatch + // between required signers as parameters and signers passed in. + assert_success!(output.status().to_owned()); + h.executor.apply_write_set(output.write_set()); + + let alice_after = read_coin(&h, alice.address()); + let bob_after = read_coin(&h, bob.address()); + + assert_eq!(alice_start, alice_after); + assert!(bob_start > bob_after); +} + +#[test] +fn test_normal_tx_with_gas_payer_insufficient_funds() { + let mut h = MoveHarness::new_with_features(vec![FeatureFlag::GAS_PAYER_ENABLED], vec![]); + + let alice = h.new_account_at(AccountAddress::from_hex_literal("0xa11ce").unwrap()); + let bob = h.new_account_with_balance_and_sequence_number(1, 0); + + // Load the code + let acc = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); + assert_success!(h.publish_package(&acc, &common::test_dir_path("string_args.data/pack"))); + + let fun: MemberId = str::parse("0xcafe::test::nothing").unwrap(); + let entry = EntryFunction::new(fun.module_id, fun.member_id, vec![], vec![]); + let transaction = TransactionBuilder::new(alice.clone()) + .secondary_signers(vec![bob]) + .entry_function(entry) + .sequence_number(h.sequence_number(alice.address()) | (1u64 << 63)) + .max_gas_amount(1_000_000) + .gas_unit_price(1) + .sign_multi_agent(); + + let output = h.executor.execute_transaction(transaction); + assert!(transaction_status_eq( + output.status(), + &TransactionStatus::Discard(StatusCode::INSUFFICIENT_BALANCE_FOR_TRANSACTION_FEE) + )); +} diff --git a/aptos-move/e2e-move-tests/src/tests/string_args.data/pack/sources/hello.move b/aptos-move/e2e-move-tests/src/tests/string_args.data/pack/sources/hello.move index 9dfdc667d7183..5a3775cf40fd3 100644 --- a/aptos-move/e2e-move-tests/src/tests/string_args.data/pack/sources/hello.move +++ b/aptos-move/e2e-move-tests/src/tests/string_args.data/pack/sources/hello.move @@ -115,6 +115,9 @@ module 0xCAFE::test { }; } + entry fun nothing() { + } + fun find_hello_in_msgs_of_msgs(msgs: &vector>) { let outer_len = vector::length(msgs); while (outer_len > 0) { diff --git a/aptos-move/framework/aptos-framework/doc/transaction_validation.md b/aptos-move/framework/aptos-framework/doc/transaction_validation.md index 04b254c45c2f6..44896c2d6c0ac 100644 --- a/aptos-move/framework/aptos-framework/doc/transaction_validation.md +++ b/aptos-move/framework/aptos-framework/doc/transaction_validation.md @@ -13,6 +13,7 @@ - [Function `script_prologue`](#0x1_transaction_validation_script_prologue) - [Function `multi_agent_script_prologue`](#0x1_transaction_validation_multi_agent_script_prologue) - [Function `epilogue`](#0x1_transaction_validation_epilogue) +- [Function `epilogue_gas_payer`](#0x1_transaction_validation_epilogue_gas_payer) - [Specification](#@Specification_1) - [Function `initialize`](#@Specification_1_initialize) - [Function `prologue_common`](#@Specification_1_prologue_common) @@ -20,6 +21,7 @@ - [Function `script_prologue`](#@Specification_1_script_prologue) - [Function `multi_agent_script_prologue`](#@Specification_1_multi_agent_script_prologue) - [Function `epilogue`](#@Specification_1_epilogue) + - [Function `epilogue_gas_payer`](#@Specification_1_epilogue_gas_payer)
use 0x1::account;
@@ -119,6 +121,16 @@ Transaction exceeded its allocated max gas
 
 
 
+
+
+MSB is used to indicate a gas payer tx
+
+
+
const GAS_PAYER_FLAG_BIT: u64 = 9223372036854775808;
+
+ + + @@ -146,6 +158,15 @@ Transaction exceeded its allocated max gas + + + + +
const PROLOGUE_EGAS_PAYER_ACCOUNT_MISSING: u64 = 1010;
+
+ + + Prologue errors. These are separated out from the other errors in this @@ -249,7 +270,7 @@ Only called during genesis to initialize system resources for this module. -
fun prologue_common(sender: signer, txn_sequence_number: u64, txn_authentication_key: vector<u8>, txn_gas_price: u64, txn_max_gas_units: u64, txn_expiration_time: u64, chain_id: u8)
+
fun prologue_common(sender: signer, gas_payer: address, txn_sequence_number: u64, txn_authentication_key: vector<u8>, txn_gas_price: u64, txn_max_gas_units: u64, txn_expiration_time: u64, chain_id: u8)
 
@@ -260,6 +281,7 @@ Only called during genesis to initialize system resources for this module.
fun prologue_common(
     sender: signer,
+    gas_payer: address,
     txn_sequence_number: u64,
     txn_authentication_key: vector<u8>,
     txn_gas_price: u64,
@@ -281,7 +303,7 @@ Only called during genesis to initialize system resources for this module.
     );
 
     assert!(
-        (txn_sequence_number as u128) < MAX_U64,
+        txn_sequence_number < GAS_PAYER_FLAG_BIT,
         error::out_of_range(PROLOGUE_ESEQUENCE_NUMBER_TOO_BIG)
     );
 
@@ -300,10 +322,10 @@ Only called during genesis to initialize system resources for this module.
 
     let max_transaction_fee = txn_gas_price * txn_max_gas_units;
     assert!(
-        coin::is_account_registered<AptosCoin>(transaction_sender),
+        coin::is_account_registered<AptosCoin>(gas_payer),
         error::invalid_argument(PROLOGUE_ECANT_PAY_GAS_DEPOSIT),
     );
-    let balance = coin::balance<AptosCoin>(transaction_sender);
+    let balance = coin::balance<AptosCoin>(gas_payer);
     assert!(balance >= max_transaction_fee, error::invalid_argument(PROLOGUE_ECANT_PAY_GAS_DEPOSIT));
 }
 
@@ -336,7 +358,8 @@ Only called during genesis to initialize system resources for this module. txn_expiration_time: u64, chain_id: u8, ) { - prologue_common(sender, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id) + let gas_payer = signer::address_of(&sender); + prologue_common(sender, gas_payer, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id) }
@@ -369,7 +392,8 @@ Only called during genesis to initialize system resources for this module. chain_id: u8, _script_hash: vector<u8>, ) { - prologue_common(sender, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id) + let gas_payer = signer::address_of(&sender); + prologue_common(sender, gas_payer, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id) }
@@ -403,9 +427,16 @@ Only called during genesis to initialize system resources for this module. txn_expiration_time: u64, chain_id: u8, ) { - prologue_common(sender, txn_sequence_number, txn_sender_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id); - + let gas_payer = signer::address_of(&sender); let num_secondary_signers = vector::length(&secondary_signer_addresses); + if (txn_sequence_number >= GAS_PAYER_FLAG_BIT) { + assert!(features::gas_payer_enabled(), error::out_of_range(PROLOGUE_ESEQUENCE_NUMBER_TOO_BIG)); + assert!(num_secondary_signers > 0, error::invalid_argument(PROLOGUE_EGAS_PAYER_ACCOUNT_MISSING)); + gas_payer = *std::vector::borrow(&secondary_signer_addresses, std::vector::length(&secondary_signer_addresses) - 1); + // Clear the high bit as it's not part of the sequence number + txn_sequence_number = txn_sequence_number - GAS_PAYER_FLAG_BIT; + }; + prologue_common(sender, gas_payer, txn_sequence_number, txn_sender_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id); assert!( vector::length(&secondary_signer_public_key_hashes) == num_secondary_signers, @@ -448,7 +479,7 @@ Epilogue function is run after a transaction is successfully executed. Called by the Adapter -
fun epilogue(account: signer, _txn_sequence_number: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
+
fun epilogue(account: signer, txn_sequence_number: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
 
@@ -459,6 +490,40 @@ Called by the Adapter
fun epilogue(
     account: signer,
+    txn_sequence_number: u64,
+    txn_gas_price: u64,
+    txn_max_gas_units: u64,
+    gas_units_remaining: u64
+) {
+    let addr = signer::address_of(&account);
+    epilogue_gas_payer(account, addr, txn_sequence_number, txn_gas_price, txn_max_gas_units, gas_units_remaining);
+}
+
+ + + + + + + +## 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, _txn_sequence_number: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
+
+ + + +
+Implementation + + +
fun epilogue_gas_payer(
+    account: signer,
+    gas_payer: address,
     _txn_sequence_number: u64,
     txn_gas_price: u64,
     txn_max_gas_units: u64,
@@ -472,26 +537,26 @@ Called by the Adapter
         error::out_of_range(EOUT_OF_GAS)
     );
     let transaction_fee_amount = txn_gas_price * gas_used;
-    let addr = signer::address_of(&account);
     // it's important to maintain the error code consistent with vm
     // to do failed transaction cleanup.
     assert!(
-        coin::balance<AptosCoin>(addr) >= transaction_fee_amount,
+        coin::balance<AptosCoin>(gas_payer) >= transaction_fee_amount,
         error::out_of_range(PROLOGUE_ECANT_PAY_GAS_DEPOSIT),
     );
 
     if (features::collect_and_distribute_gas_fees()) {
         // If transaction fees are redistributed to validators, collect them here for
         // later redistribution.
-        transaction_fee::collect_fee(addr, transaction_fee_amount);
+        transaction_fee::collect_fee(gas_payer, transaction_fee_amount);
     } else {
         // Otherwise, just burn the fee.
         // TODO: this branch should be removed completely when transaction fee collection
         // is tested and is fully proven to work well.
-        transaction_fee::burn_fee(addr, transaction_fee_amount);
+        transaction_fee::burn_fee(gas_payer, transaction_fee_amount);
     };
 
     // Increment sequence number
+    let addr = signer::address_of(&account);
     account::increment_sequence_number(addr);
 }
 
@@ -540,6 +605,7 @@ Give some constraints that may abort according to the conditions.
schema PrologueCommonAbortsIf {
     sender: signer;
+    gas_payer: address;
     txn_sequence_number: u64;
     txn_authentication_key: vector<u8>;
     txn_gas_price: u64;
@@ -554,12 +620,12 @@ Give some constraints that may abort according to the conditions.
     aborts_if !account::exists_at(transaction_sender);
     aborts_if !(txn_sequence_number >= global<Account>(transaction_sender).sequence_number);
     aborts_if !(txn_authentication_key == global<Account>(transaction_sender).authentication_key);
-    aborts_if !(txn_sequence_number < MAX_U64);
+    aborts_if !(txn_sequence_number < GAS_PAYER_FLAG_BIT);
     let max_transaction_fee = txn_gas_price * txn_max_gas_units;
     aborts_if max_transaction_fee > MAX_U64;
     aborts_if !(txn_sequence_number == global<Account>(transaction_sender).sequence_number);
-    aborts_if !exists<CoinStore<AptosCoin>>(transaction_sender);
-    aborts_if !(global<CoinStore<AptosCoin>>(transaction_sender).coin.value >= max_transaction_fee);
+    aborts_if !exists<CoinStore<AptosCoin>>(gas_payer);
+    aborts_if !(global<CoinStore<AptosCoin>>(gas_payer).coin.value >= max_transaction_fee);
 }
 
@@ -570,7 +636,7 @@ Give some constraints that may abort according to the conditions. ### Function `prologue_common` -
fun prologue_common(sender: signer, txn_sequence_number: u64, txn_authentication_key: vector<u8>, txn_gas_price: u64, txn_max_gas_units: u64, txn_expiration_time: u64, chain_id: u8)
+
fun prologue_common(sender: signer, gas_payer: address, txn_sequence_number: u64, txn_authentication_key: vector<u8>, txn_gas_price: u64, txn_max_gas_units: u64, txn_expiration_time: u64, chain_id: u8)
 
@@ -593,6 +659,7 @@ Give some constraints that may abort according to the conditions.
include PrologueCommonAbortsIf {
+    gas_payer: signer::address_of(sender),
     txn_authentication_key: txn_public_key
 };
 
@@ -611,6 +678,7 @@ Give some constraints that may abort according to the conditions.
include PrologueCommonAbortsIf {
+    gas_payer: signer::address_of(sender),
     txn_authentication_key: txn_public_key
 };
 
@@ -630,7 +698,21 @@ Aborts if length of public key hashed vector not equal the number of singers. -
include PrologueCommonAbortsIf {
+
let gas_payer = if (txn_sequence_number < GAS_PAYER_FLAG_BIT) {
+    signer::address_of(sender)
+} else {
+    secondary_signer_addresses[len(secondary_signer_addresses) - 1]
+};
+aborts_if txn_sequence_number >= GAS_PAYER_FLAG_BIT && !features::spec_gas_payer_enabled();
+aborts_if txn_sequence_number >= GAS_PAYER_FLAG_BIT && len(secondary_signer_addresses) == 0;
+let adjusted_txn_sequence_number = if (txn_sequence_number >= GAS_PAYER_FLAG_BIT) {
+    txn_sequence_number - GAS_PAYER_FLAG_BIT
+} else {
+    txn_sequence_number
+};
+include PrologueCommonAbortsIf {
+    gas_payer,
+    txn_sequence_number: adjusted_txn_sequence_number,
     txn_authentication_key: txn_sender_public_key
 };
 let num_secondary_signers = len(secondary_signer_addresses);
@@ -652,7 +734,7 @@ not equal the number of singers.
 ### Function `epilogue`
 
 
-
fun epilogue(account: signer, _txn_sequence_number: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
+
fun epilogue(account: signer, txn_sequence_number: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
 
@@ -704,4 +786,62 @@ Skip transaction_fee::burn_fee verification.
+ + + +### Function `epilogue_gas_payer` + + +
fun epilogue_gas_payer(account: signer, gas_payer: address, _txn_sequence_number: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64)
+
+ + +Abort according to the conditions. +AptosCoinCapabilities and CoinInfo should exists. +Skip transaction_fee::burn_fee verification. + + +
aborts_if !(txn_max_gas_units >= gas_units_remaining);
+let gas_used = txn_max_gas_units - gas_units_remaining;
+aborts_if !(txn_gas_price * gas_used <= MAX_U64);
+let transaction_fee_amount = txn_gas_price * gas_used;
+let addr = signer::address_of(account);
+aborts_if !exists<CoinStore<AptosCoin>>(gas_payer);
+aborts_if !(global<CoinStore<AptosCoin>>(gas_payer).coin.value >= transaction_fee_amount);
+aborts_if !exists<Account>(addr);
+aborts_if !(global<Account>(addr).sequence_number < MAX_U64);
+let pre_balance = global<coin::CoinStore<AptosCoin>>(gas_payer).coin.value;
+let post balance = global<coin::CoinStore<AptosCoin>>(gas_payer).coin.value;
+let pre_account = global<account::Account>(addr);
+let post account = global<account::Account>(addr);
+ensures balance == pre_balance - transaction_fee_amount;
+ensures account.sequence_number == pre_account.sequence_number + 1;
+let collected_fees = global<CollectedFeesPerBlock>(@aptos_framework).amount;
+let aggr = collected_fees.value;
+let aggr_val = aggregator::spec_aggregator_get_val(aggr);
+let aggr_lim = aggregator::spec_get_limit(aggr);
+let aptos_addr = type_info::type_of<AptosCoin>().account_address;
+let apt_addr = type_info::type_of<AptosCoin>().account_address;
+let maybe_apt_supply = global<CoinInfo<AptosCoin>>(apt_addr).supply;
+let apt_supply = option::spec_borrow(maybe_apt_supply);
+let apt_supply_value = optional_aggregator::optional_aggregator_value(apt_supply);
+aborts_if if (features::spec_is_enabled(features::COLLECT_AND_DISTRIBUTE_GAS_FEES)) {
+    !exists<CollectedFeesPerBlock>(@aptos_framework)
+        || transaction_fee_amount > 0 &&
+        ( // `exists<CoinStore<AptosCoin>>(addr)` checked above.
+            // Sufficiency of funds is checked above.
+            aggr_val + transaction_fee_amount > aggr_lim
+                || aggr_val + transaction_fee_amount > MAX_U128)
+} else {
+    // Existence of CoinStore in `addr` is checked above.
+    // Sufficiency of funds is checked above.
+    !exists<AptosCoinCapabilities>(@aptos_framework) ||
+        // Existence of APT's CoinInfo
+        transaction_fee_amount > 0 && !exists<CoinInfo<AptosCoin>>(aptos_addr) ||
+        // Sufficiency of APT's supply
+        option::spec_is_some(maybe_apt_supply) && apt_supply_value < transaction_fee_amount
+};
+
+ + [move-book]: https://aptos.dev/guides/move-guides/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 65982fd8904f9..d2f96bf7877cc 100644 --- a/aptos-move/framework/aptos-framework/sources/transaction_validation.move +++ b/aptos-move/framework/aptos-framework/sources/transaction_validation.move @@ -25,6 +25,8 @@ module aptos_framework::transaction_validation { user_epilogue_name: vector, } + /// MSB is used to indicate a gas payer tx + const GAS_PAYER_FLAG_BIT: u64 = 1u64 << 63; const MAX_U64: u128 = 18446744073709551615; /// Transaction exceeded its allocated max gas @@ -42,6 +44,7 @@ module aptos_framework::transaction_validation { const PROLOGUE_EBAD_CHAIN_ID: u64 = 1007; const PROLOGUE_ESEQUENCE_NUMBER_TOO_BIG: u64 = 1008; const PROLOGUE_ESECONDARY_KEYS_ADDRESSES_COUNT_MISMATCH: u64 = 1009; + const PROLOGUE_EGAS_PAYER_ACCOUNT_MISSING: u64 = 1010; /// Only called during genesis to initialize system resources for this module. public(friend) fun initialize( @@ -65,6 +68,7 @@ module aptos_framework::transaction_validation { fun prologue_common( sender: signer, + gas_payer: address, txn_sequence_number: u64, txn_authentication_key: vector, txn_gas_price: u64, @@ -86,7 +90,7 @@ module aptos_framework::transaction_validation { ); assert!( - (txn_sequence_number as u128) < MAX_U64, + txn_sequence_number < GAS_PAYER_FLAG_BIT, error::out_of_range(PROLOGUE_ESEQUENCE_NUMBER_TOO_BIG) ); @@ -105,10 +109,10 @@ module aptos_framework::transaction_validation { let max_transaction_fee = txn_gas_price * txn_max_gas_units; assert!( - coin::is_account_registered(transaction_sender), + coin::is_account_registered(gas_payer), error::invalid_argument(PROLOGUE_ECANT_PAY_GAS_DEPOSIT), ); - let balance = coin::balance(transaction_sender); + let balance = coin::balance(gas_payer); assert!(balance >= max_transaction_fee, error::invalid_argument(PROLOGUE_ECANT_PAY_GAS_DEPOSIT)); } @@ -121,7 +125,8 @@ module aptos_framework::transaction_validation { txn_expiration_time: u64, chain_id: u8, ) { - prologue_common(sender, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id) + let gas_payer = signer::address_of(&sender); + prologue_common(sender, gas_payer, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id) } fun script_prologue( @@ -134,7 +139,8 @@ module aptos_framework::transaction_validation { chain_id: u8, _script_hash: vector, ) { - prologue_common(sender, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id) + let gas_payer = signer::address_of(&sender); + prologue_common(sender, gas_payer, txn_sequence_number, txn_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id) } fun multi_agent_script_prologue( @@ -148,9 +154,16 @@ module aptos_framework::transaction_validation { txn_expiration_time: u64, chain_id: u8, ) { - prologue_common(sender, txn_sequence_number, txn_sender_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id); - + let gas_payer = signer::address_of(&sender); let num_secondary_signers = vector::length(&secondary_signer_addresses); + if (txn_sequence_number >= GAS_PAYER_FLAG_BIT) { + assert!(features::gas_payer_enabled(), error::out_of_range(PROLOGUE_ESEQUENCE_NUMBER_TOO_BIG)); + assert!(num_secondary_signers > 0, error::invalid_argument(PROLOGUE_EGAS_PAYER_ACCOUNT_MISSING)); + gas_payer = *std::vector::borrow(&secondary_signer_addresses, std::vector::length(&secondary_signer_addresses) - 1); + // Clear the high bit as it's not part of the sequence number + txn_sequence_number = txn_sequence_number - GAS_PAYER_FLAG_BIT; + }; + prologue_common(sender, gas_payer, txn_sequence_number, txn_sender_public_key, txn_gas_price, txn_max_gas_units, txn_expiration_time, chain_id); assert!( vector::length(&secondary_signer_public_key_hashes) == num_secondary_signers, @@ -184,6 +197,20 @@ module aptos_framework::transaction_validation { /// Called by the Adapter fun epilogue( account: signer, + txn_sequence_number: u64, + txn_gas_price: u64, + txn_max_gas_units: u64, + gas_units_remaining: u64 + ) { + let addr = signer::address_of(&account); + epilogue_gas_payer(account, addr, txn_sequence_number, 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( + account: signer, + gas_payer: address, _txn_sequence_number: u64, txn_gas_price: u64, txn_max_gas_units: u64, @@ -197,26 +224,26 @@ module aptos_framework::transaction_validation { error::out_of_range(EOUT_OF_GAS) ); let transaction_fee_amount = txn_gas_price * gas_used; - let addr = signer::address_of(&account); // it's important to maintain the error code consistent with vm // to do failed transaction cleanup. assert!( - coin::balance(addr) >= transaction_fee_amount, + coin::balance(gas_payer) >= transaction_fee_amount, error::out_of_range(PROLOGUE_ECANT_PAY_GAS_DEPOSIT), ); if (features::collect_and_distribute_gas_fees()) { // If transaction fees are redistributed to validators, collect them here for // later redistribution. - transaction_fee::collect_fee(addr, transaction_fee_amount); + transaction_fee::collect_fee(gas_payer, transaction_fee_amount); } else { // Otherwise, just burn the fee. // TODO: this branch should be removed completely when transaction fee collection // is tested and is fully proven to work well. - transaction_fee::burn_fee(addr, transaction_fee_amount); + transaction_fee::burn_fee(gas_payer, transaction_fee_amount); }; // Increment sequence number + let addr = signer::address_of(&account); account::increment_sequence_number(addr); } } 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 ac42a7e1507ed..b997af5fad0f4 100644 --- a/aptos-move/framework/aptos-framework/sources/transaction_validation.spec.move +++ b/aptos-move/framework/aptos-framework/sources/transaction_validation.spec.move @@ -27,6 +27,7 @@ spec aptos_framework::transaction_validation { use aptos_framework::account::{Account}; use aptos_framework::coin::{CoinStore}; sender: signer; + gas_payer: address; txn_sequence_number: u64; txn_authentication_key: vector; txn_gas_price: u64; @@ -43,17 +44,18 @@ spec aptos_framework::transaction_validation { aborts_if !account::exists_at(transaction_sender); aborts_if !(txn_sequence_number >= global(transaction_sender).sequence_number); aborts_if !(txn_authentication_key == global(transaction_sender).authentication_key); - aborts_if !(txn_sequence_number < MAX_U64); + aborts_if !(txn_sequence_number < GAS_PAYER_FLAG_BIT); let max_transaction_fee = txn_gas_price * txn_max_gas_units; aborts_if max_transaction_fee > MAX_U64; aborts_if !(txn_sequence_number == global(transaction_sender).sequence_number); - aborts_if !exists>(transaction_sender); - aborts_if !(global>(transaction_sender).coin.value >= max_transaction_fee); + aborts_if !exists>(gas_payer); + aborts_if !(global>(gas_payer).coin.value >= max_transaction_fee); } spec prologue_common( sender: signer, + gas_payer: address, txn_sequence_number: u64, txn_authentication_key: vector, txn_gas_price: u64, @@ -74,6 +76,7 @@ spec aptos_framework::transaction_validation { chain_id: u8, ) { include PrologueCommonAbortsIf { + gas_payer: signer::address_of(sender), txn_authentication_key: txn_public_key }; } @@ -89,6 +92,7 @@ spec aptos_framework::transaction_validation { _script_hash: vector, ) { include PrologueCommonAbortsIf { + gas_payer: signer::address_of(sender), txn_authentication_key: txn_public_key }; } @@ -106,7 +110,21 @@ spec aptos_framework::transaction_validation { txn_expiration_time: u64, chain_id: u8, ) { + let gas_payer = if (txn_sequence_number < GAS_PAYER_FLAG_BIT) { + signer::address_of(sender) + } else { + secondary_signer_addresses[len(secondary_signer_addresses) - 1] + }; + aborts_if txn_sequence_number >= GAS_PAYER_FLAG_BIT && !features::spec_gas_payer_enabled(); + aborts_if txn_sequence_number >= GAS_PAYER_FLAG_BIT && len(secondary_signer_addresses) == 0; + let adjusted_txn_sequence_number = if (txn_sequence_number >= GAS_PAYER_FLAG_BIT) { + txn_sequence_number - GAS_PAYER_FLAG_BIT + } else { + txn_sequence_number + }; include PrologueCommonAbortsIf { + gas_payer, + txn_sequence_number: adjusted_txn_sequence_number, txn_authentication_key: txn_sender_public_key }; @@ -132,7 +150,7 @@ spec aptos_framework::transaction_validation { /// Skip transaction_fee::burn_fee verification. spec epilogue( account: signer, - _txn_sequence_number: u64, + txn_sequence_number: u64, txn_gas_price: u64, txn_max_gas_units: u64, gas_units_remaining: u64 @@ -197,4 +215,76 @@ spec aptos_framework::transaction_validation { option::spec_is_some(maybe_apt_supply) && apt_supply_value < transaction_fee_amount }; } + + /// Abort according to the conditions. + /// `AptosCoinCapabilities` and `CoinInfo` should exists. + /// Skip transaction_fee::burn_fee verification. + spec epilogue_gas_payer( + account: signer, + gas_payer: address, + _txn_sequence_number: u64, + txn_gas_price: u64, + txn_max_gas_units: u64, + gas_units_remaining: u64 + ) { + use std::option; + use aptos_std::type_info; + use aptos_framework::account::{Account}; + use aptos_framework::aggregator; + use aptos_framework::aptos_coin::{AptosCoin}; + use aptos_framework::coin::{CoinStore, CoinInfo}; + use aptos_framework::optional_aggregator; + use aptos_framework::transaction_fee::{AptosCoinCapabilities, CollectedFeesPerBlock}; + + aborts_if !(txn_max_gas_units >= gas_units_remaining); + let gas_used = txn_max_gas_units - gas_units_remaining; + + aborts_if !(txn_gas_price * gas_used <= MAX_U64); + let transaction_fee_amount = txn_gas_price * gas_used; + + let addr = signer::address_of(account); + aborts_if !exists>(gas_payer); + // Sufficiency of funds + aborts_if !(global>(gas_payer).coin.value >= transaction_fee_amount); + + aborts_if !exists(addr); + aborts_if !(global(addr).sequence_number < MAX_U64); + + let pre_balance = global>(gas_payer).coin.value; + let post balance = global>(gas_payer).coin.value; + let pre_account = global(addr); + let post account = global(addr); + ensures balance == pre_balance - transaction_fee_amount; + ensures account.sequence_number == pre_account.sequence_number + 1; + + + // Bindings for `collect_fee` verification. + let collected_fees = global(@aptos_framework).amount; + let aggr = collected_fees.value; + let aggr_val = aggregator::spec_aggregator_get_val(aggr); + let aggr_lim = aggregator::spec_get_limit(aggr); + let aptos_addr = type_info::type_of().account_address; + // Bindings for `burn_fee` verification. + let apt_addr = type_info::type_of().account_address; + let maybe_apt_supply = global>(apt_addr).supply; + let apt_supply = option::spec_borrow(maybe_apt_supply); + let apt_supply_value = optional_aggregator::optional_aggregator_value(apt_supply); + // N.B.: Why can't `features::is_enabled` + aborts_if if (features::spec_is_enabled(features::COLLECT_AND_DISTRIBUTE_GAS_FEES)) { + !exists(@aptos_framework) + || transaction_fee_amount > 0 && + ( // `exists>(addr)` checked above. + // Sufficiency of funds is checked above. + aggr_val + transaction_fee_amount > aggr_lim + || aggr_val + transaction_fee_amount > MAX_U128) + } else { + // Existence of CoinStore in `addr` is checked above. + // Sufficiency of funds is checked above. + !exists(@aptos_framework) || + // Existence of APT's CoinInfo + transaction_fee_amount > 0 && !exists>(aptos_addr) || + // Sufficiency of APT's supply + option::spec_is_some(maybe_apt_supply) && apt_supply_value < transaction_fee_amount + }; + } } diff --git a/aptos-move/framework/move-stdlib/doc/features.md b/aptos-move/framework/move-stdlib/doc/features.md index 38c5f5a667623..d1c4ce9535541 100644 --- a/aptos-move/framework/move-stdlib/doc/features.md +++ b/aptos-move/framework/move-stdlib/doc/features.md @@ -61,6 +61,7 @@ return true. - [Function `partial_governance_voting_enabled`](#0x1_features_partial_governance_voting_enabled) - [Function `get_delegation_pool_partial_governance_voting`](#0x1_features_get_delegation_pool_partial_governance_voting) - [Function `delegation_pool_partial_governance_voting_enabled`](#0x1_features_delegation_pool_partial_governance_voting_enabled) +- [Function `gas_payer_enabled`](#0x1_features_gas_payer_enabled) - [Function `change_feature_flags`](#0x1_features_change_feature_flags) - [Function `is_enabled`](#0x1_features_is_enabled) - [Function `set`](#0x1_features_set) @@ -239,6 +240,17 @@ The provided signer has not a framework address. + + +Whether alternate gas payer is supported +Lifetime: transient + + +
const GAS_PAYER_ENABLED: u64 = 22;
+
+ + + Whether multisig accounts (different from accounts with multi-ed25519 auth keys) are enabled. @@ -1031,6 +1043,30 @@ Lifetime: transient +
+ + + +## Function `gas_payer_enabled` + + + +
public fun gas_payer_enabled(): bool
+
+ + + +
+Implementation + + +
public fun gas_payer_enabled(): bool acquires Features {
+    is_enabled(GAS_PAYER_ENABLED)
+}
+
+ + +
@@ -1290,6 +1326,17 @@ Helper to check whether a feature flag is enabled. + + + + +
fun spec_gas_payer_enabled(): bool {
+   spec_is_enabled(GAS_PAYER_ENABLED)
+}
+
+ + + ### Function `set` diff --git a/aptos-move/framework/move-stdlib/doc/fixed_point32.md b/aptos-move/framework/move-stdlib/doc/fixed_point32.md index c3689437d52dd..36fab387c6215 100644 --- a/aptos-move/framework/move-stdlib/doc/fixed_point32.md +++ b/aptos-move/framework/move-stdlib/doc/fixed_point32.md @@ -819,10 +819,8 @@ Returns the value of a FixedPoint32 to the nearest integer. -TODO: worked in the past but started to time out since last z3 update - -
pragma verify = false;
+
pragma verify_duration_estimate = 120;
 pragma opaque;
 aborts_if false;
 ensures result == spec_ceil(num);
@@ -858,7 +856,8 @@ TODO: worked in the past but started to time out since last z3 update
 
 
 
-
pragma opaque;
+
pragma verify_duration_estimate = 120;
+pragma opaque;
 aborts_if false;
 ensures result == spec_round(num);
 
diff --git a/aptos-move/framework/move-stdlib/sources/configs/features.move b/aptos-move/framework/move-stdlib/sources/configs/features.move index 21a18f27b3847..1e96cfb53cd21 100644 --- a/aptos-move/framework/move-stdlib/sources/configs/features.move +++ b/aptos-move/framework/move-stdlib/sources/configs/features.move @@ -194,6 +194,13 @@ module std::features { is_enabled(DELEGATION_POOL_PARTIAL_GOVERNANCE_VOTING) } + /// Whether alternate gas payer is supported + /// Lifetime: transient + const GAS_PAYER_ENABLED: u64 = 22; + public fun gas_payer_enabled(): bool acquires Features { + is_enabled(GAS_PAYER_ENABLED) + } + // ============================================================================================ // Feature Flag Implementation diff --git a/aptos-move/framework/move-stdlib/sources/configs/features.spec.move b/aptos-move/framework/move-stdlib/sources/configs/features.spec.move index 696926524685b..108a6fb9dfb13 100644 --- a/aptos-move/framework/move-stdlib/sources/configs/features.spec.move +++ b/aptos-move/framework/move-stdlib/sources/configs/features.spec.move @@ -41,6 +41,10 @@ spec std::features { spec_is_enabled(PERIODICAL_REWARD_RATE_DECREASE) } + spec fun spec_gas_payer_enabled(): bool { + spec_is_enabled(GAS_PAYER_ENABLED) + } + spec periodical_reward_rate_decrease_enabled { pragma opaque; aborts_if [abstract] false; diff --git a/aptos-move/framework/move-stdlib/sources/fixed_point32.move b/aptos-move/framework/move-stdlib/sources/fixed_point32.move index 4fb3d0531ba24..96409a9ac4dfd 100644 --- a/aptos-move/framework/move-stdlib/sources/fixed_point32.move +++ b/aptos-move/framework/move-stdlib/sources/fixed_point32.move @@ -243,8 +243,7 @@ module std::fixed_point32 { (val >> 32 as u64) } spec ceil { - /// TODO: worked in the past but started to time out since last z3 update - pragma verify = false; + pragma verify_duration_estimate = 120; pragma opaque; aborts_if false; ensures result == spec_ceil(num); @@ -270,6 +269,7 @@ module std::fixed_point32 { } } spec round { + pragma verify_duration_estimate = 120; pragma opaque; aborts_if false; ensures result == spec_round(num); 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 e0b1e67844fb5..97f67a47e1c8e 100644 --- a/third_party/move/move-core/types/src/vm_status.rs +++ b/third_party/move/move-core/types/src/vm_status.rs @@ -565,8 +565,8 @@ pub enum StatusCode { MULTISIG_TRANSACTION_NOT_FOUND = 33, MULTISIG_TRANSACTION_INSUFFICIENT_APPROVALS = 34, MULTISIG_TRANSACTION_PAYLOAD_DOES_NOT_MATCH_HASH = 35, + GAS_PAYER_ACCOUNT_MISSING = 36, // Reserved error code for future use - RESERVED_VALIDATION_ERROR_1 = 36, RESERVED_VALIDATION_ERROR_2 = 37, RESERVED_VALIDATION_ERROR_3 = 38, RESERVED_VALIDATION_ERROR_4 = 39, diff --git a/types/src/account_config/resources/transaction_validation.rs b/types/src/account_config/resources/transaction_validation.rs index 5a0991c9801f7..1381e2a0672a8 100644 --- a/types/src/account_config/resources/transaction_validation.rs +++ b/types/src/account_config/resources/transaction_validation.rs @@ -23,6 +23,7 @@ pub static APTOS_TRANSACTION_VALIDATION: Lazy = module_prologue_name: Identifier::new("module_prologue").unwrap(), multi_agent_prologue_name: Identifier::new("multi_agent_script_prologue").unwrap(), user_epilogue_name: Identifier::new("epilogue").unwrap(), + user_epilogue_gas_payer_name: Identifier::new("epilogue_gas_payer").unwrap(), }); /// A Rust representation of chain-specific account information @@ -35,6 +36,7 @@ pub struct TransactionValidation { pub module_prologue_name: Identifier, pub multi_agent_prologue_name: Identifier, pub user_epilogue_name: Identifier, + pub user_epilogue_gas_payer_name: Identifier, } impl TransactionValidation { diff --git a/types/src/on_chain_config/aptos_features.rs b/types/src/on_chain_config/aptos_features.rs index 7866ab0ae9e14..a7b10862be49b 100644 --- a/types/src/on_chain_config/aptos_features.rs +++ b/types/src/on_chain_config/aptos_features.rs @@ -29,6 +29,7 @@ pub enum FeatureFlag { STORAGE_SLOT_METADATA = 19, CHARGE_INVARIANT_VIOLATION = 20, DELEGATION_POOL_PARTIAL_GOVERNANCE_VOTING = 21, + GAS_PAYER_ENABLED = 22, } /// Representation of features on chain as a bitset.