diff --git a/aptos-move/framework/aptos-framework/doc/transaction_validation.md b/aptos-move/framework/aptos-framework/doc/transaction_validation.md index 90b0e01a7a56f..77c61310c5150 100644 --- a/aptos-move/framework/aptos-framework/doc/transaction_validation.md +++ b/aptos-move/framework/aptos-framework/doc/transaction_validation.md @@ -6,7 +6,10 @@ - [Resource `TransactionValidation`](#0x1_transaction_validation_TransactionValidation) +- [Struct `GasPermission`](#0x1_transaction_validation_GasPermission) - [Constants](#@Constants_0) +- [Function `grant_gas_permission`](#0x1_transaction_validation_grant_gas_permission) +- [Function `revoke_gas_permission`](#0x1_transaction_validation_revoke_gas_permission) - [Function `initialize`](#0x1_transaction_validation_initialize) - [Function `prologue_common`](#0x1_transaction_validation_prologue_common) - [Function `script_prologue`](#0x1_transaction_validation_script_prologue) @@ -28,6 +31,8 @@ - [Specification](#@Specification_1) - [High-level Requirements](#high-level-req) - [Module-level Specification](#module-level-spec) + - [Function `grant_gas_permission`](#@Specification_1_grant_gas_permission) + - [Function `revoke_gas_permission`](#@Specification_1_revoke_gas_permission) - [Function `initialize`](#@Specification_1_initialize) - [Function `prologue_common`](#@Specification_1_prologue_common) - [Function `script_prologue`](#@Specification_1_script_prologue) @@ -57,6 +62,7 @@ use 0x1::error; use 0x1::features; use 0x1::option; +use 0x1::permissioned_signer; use 0x1::signer; use 0x1::system_addresses; use 0x1::timestamp; @@ -123,6 +129,33 @@ correct chain-specific prologue and epilogue functions + + + + +## Struct `GasPermission` + + + +
struct GasPermission has copy, drop, store
+
+ + + +
+Fields + + +
+
+dummy_field: bool +
+
+ +
+
+ +
@@ -243,6 +276,76 @@ important to the semantics of the system. + + + + +
const PROLOGUE_PERMISSIONED_GAS_LIMIT_INSUFFICIENT: u64 = 1011;
+
+ + + + + +## Function `grant_gas_permission` + +Permission management + +Master signer grant permissioned signer ability to consume a given amount of gas in octas. + + +
public fun grant_gas_permission(master: &signer, permissioned: &signer, gas_amount: u64)
+
+ + + +
+Implementation + + +
public fun grant_gas_permission(
+    master: &signer,
+    permissioned: &signer,
+    gas_amount: u64
+) {
+    permissioned_signer::authorize_increase(
+        master,
+        permissioned,
+        (gas_amount as u256),
+        GasPermission {}
+    )
+}
+
+ + + +
+ + + +## Function `revoke_gas_permission` + +Removing permissions from permissioned signer. + + +
public fun revoke_gas_permission(permissioned: &signer)
+
+ + + +
+Implementation + + +
public fun revoke_gas_permission(permissioned: &signer) {
+    permissioned_signer::revoke_permission(permissioned, GasPermission {})
+}
+
+ + + +
+ ## Function `initialize` @@ -382,6 +485,14 @@ Only called during genesis to initialize system resources for this module. is_simulation, gas_payer_address )) { + assert!( + permissioned_signer::check_permission_capacity_above( + gas_payer, + (max_transaction_fee as u256), + GasPermission {} + ), + error::permission_denied(PROLOGUE_PERMISSIONED_GAS_LIMIT_INSUFFICIENT) + ); if (features::operations_default_to_fa_apt_store_enabled()) { assert!( aptos_account::is_fungible_balance_at_least(gas_payer_address, max_transaction_fee), @@ -965,7 +1076,7 @@ Called by the Adapter transaction_fee::burn_fee(gas_payer, burn_amount); } else if (transaction_fee_amount < storage_fee_refunded) { let mint_amount = storage_fee_refunded - transaction_fee_amount; - transaction_fee::mint_and_refund(gas_payer, mint_amount) + transaction_fee::mint_and_refund(gas_payer, mint_amount); }; }; @@ -1195,9 +1306,19 @@ If there is no fee_payer, fee_payer = sender if (transaction_fee_amount > storage_fee_refunded) { let burn_amount = transaction_fee_amount - storage_fee_refunded; transaction_fee::burn_fee(gas_payer_address, burn_amount); + permissioned_signer::check_permission_consume( + &gas_payer, + (burn_amount as u256), + GasPermission {} + ); } else if (transaction_fee_amount < storage_fee_refunded) { let mint_amount = storage_fee_refunded - transaction_fee_amount; - transaction_fee::mint_and_refund(gas_payer_address, mint_amount) + transaction_fee::mint_and_refund(gas_payer_address, mint_amount); + permissioned_signer::increase_limit( + &gas_payer, + (mint_amount as u256), + GasPermission {} + ); }; }; @@ -1267,6 +1388,38 @@ If there is no fee_payer, fee_payer = sender + + +### Function `grant_gas_permission` + + +
public fun grant_gas_permission(master: &signer, permissioned: &signer, gas_amount: u64)
+
+ + + + +
pragma aborts_if_is_partial;
+
+ + + + + +### Function `revoke_gas_permission` + + +
public fun revoke_gas_permission(permissioned: &signer)
+
+ + + + +
pragma aborts_if_is_partial;
+
+ + + ### Function `initialize` @@ -1287,58 +1440,6 @@ Aborts if TransactionValidation already exists. -Create a schema to reuse some code. -Give some constraints that may abort according to the conditions. - - - - - -
schema PrologueCommonAbortsIf {
-    sender: &signer;
-    gas_payer: &signer;
-    txn_sequence_number: u64;
-    txn_authentication_key: Option<vector<u8>>;
-    txn_gas_price: u64;
-    txn_max_gas_units: u64;
-    txn_expiration_time: u64;
-    chain_id: u8;
-    aborts_if !exists<CurrentTimeMicroseconds>(@aptos_framework);
-    aborts_if !(timestamp::now_seconds() < txn_expiration_time);
-    aborts_if !exists<ChainId>(@aptos_framework);
-    aborts_if !(chain_id::get() == chain_id);
-    let transaction_sender = signer::address_of(sender);
-    let gas_payer_addr = signer::address_of(gas_payer);
-    aborts_if (
-        !features::spec_is_enabled(features::SPONSORED_AUTOMATIC_ACCOUNT_CREATION)
-            || account::exists_at(transaction_sender)
-            || transaction_sender == gas_payer_addr
-            || txn_sequence_number > 0
-    ) && (
-        !(txn_sequence_number >= global<Account>(transaction_sender).sequence_number)
-            || !(option::spec_is_none(txn_authentication_key) || option::spec_borrow(
-            txn_authentication_key
-        ) == global<Account>(transaction_sender).authentication_key)
-            || !account::exists_at(transaction_sender)
-            || !(txn_sequence_number == global<Account>(transaction_sender).sequence_number)
-    );
-    aborts_if features::spec_is_enabled(features::SPONSORED_AUTOMATIC_ACCOUNT_CREATION)
-        && transaction_sender != gas_payer_addr
-        && txn_sequence_number == 0
-        && !account::exists_at(transaction_sender)
-        && (option::spec_is_none(txn_authentication_key) || option::spec_borrow(
-        txn_authentication_key
-    ) != bcs::to_bytes(transaction_sender));
-    aborts_if !(txn_sequence_number < (1u64 << 63));
-    let max_transaction_fee = txn_gas_price * txn_max_gas_units;
-    aborts_if max_transaction_fee > MAX_U64;
-    aborts_if !exists<CoinStore<AptosCoin>>(gas_payer_addr);
-    // This enforces high-level requirement 1:
-    aborts_if !(global<CoinStore<AptosCoin>>(gas_payer_addr).coin.value >= max_transaction_fee);
-}
-
- - diff --git a/aptos-move/framework/aptos-framework/sources/transaction_validation.move b/aptos-move/framework/aptos-framework/sources/transaction_validation.move index c70a191e1784f..87c06a9113431 100644 --- a/aptos-move/framework/aptos-framework/sources/transaction_validation.move +++ b/aptos-move/framework/aptos-framework/sources/transaction_validation.move @@ -14,6 +14,7 @@ module aptos_framework::transaction_validation { use aptos_framework::chain_id; use aptos_framework::coin; use aptos_framework::create_signer; + use aptos_framework::permissioned_signer; use aptos_framework::system_addresses; use aptos_framework::timestamp; use aptos_framework::transaction_fee; @@ -32,6 +33,8 @@ module aptos_framework::transaction_validation { user_epilogue_name: vector, } + struct GasPermission has copy, drop, store {} + /// MSB is used to indicate a gas payer tx const MAX_U64: u128 = 18446744073709551615; @@ -51,6 +54,28 @@ module aptos_framework::transaction_validation { const PROLOGUE_ESEQUENCE_NUMBER_TOO_BIG: u64 = 1008; const PROLOGUE_ESECONDARY_KEYS_ADDRESSES_COUNT_MISMATCH: u64 = 1009; const PROLOGUE_EFEE_PAYER_NOT_ENABLED: u64 = 1010; + const PROLOGUE_PERMISSIONED_GAS_LIMIT_INSUFFICIENT: u64 = 1011; + + /// Permission management + /// + /// Master signer grant permissioned signer ability to consume a given amount of gas in octas. + public fun grant_gas_permission( + master: &signer, + permissioned: &signer, + gas_amount: u64 + ) { + permissioned_signer::authorize_increase( + master, + permissioned, + (gas_amount as u256), + GasPermission {} + ) + } + + /// Removing permissions from permissioned signer. + public fun revoke_gas_permission(permissioned: &signer) { + permissioned_signer::revoke_permission(permissioned, GasPermission {}) + } /// Only called during genesis to initialize system resources for this module. public(friend) fun initialize( @@ -156,6 +181,14 @@ module aptos_framework::transaction_validation { is_simulation, gas_payer_address )) { + assert!( + permissioned_signer::check_permission_capacity_above( + gas_payer, + (max_transaction_fee as u256), + GasPermission {} + ), + error::permission_denied(PROLOGUE_PERMISSIONED_GAS_LIMIT_INSUFFICIENT) + ); if (features::operations_default_to_fa_apt_store_enabled()) { assert!( aptos_account::is_fungible_balance_at_least(gas_payer_address, max_transaction_fee), @@ -534,7 +567,7 @@ module aptos_framework::transaction_validation { transaction_fee::burn_fee(gas_payer, burn_amount); } else if (transaction_fee_amount < storage_fee_refunded) { let mint_amount = storage_fee_refunded - transaction_fee_amount; - transaction_fee::mint_and_refund(gas_payer, mint_amount) + transaction_fee::mint_and_refund(gas_payer, mint_amount); }; }; @@ -667,9 +700,19 @@ module aptos_framework::transaction_validation { if (transaction_fee_amount > storage_fee_refunded) { let burn_amount = transaction_fee_amount - storage_fee_refunded; transaction_fee::burn_fee(gas_payer_address, burn_amount); + permissioned_signer::check_permission_consume( + &gas_payer, + (burn_amount as u256), + GasPermission {} + ); } else if (transaction_fee_amount < storage_fee_refunded) { let mint_amount = storage_fee_refunded - transaction_fee_amount; - transaction_fee::mint_and_refund(gas_payer_address, mint_amount) + transaction_fee::mint_and_refund(gas_payer_address, mint_amount); + permissioned_signer::increase_limit( + &gas_payer, + (mint_amount as u256), + GasPermission {} + ); }; }; 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 461be606cba87..ec657e448c3d1 100644 --- a/aptos-move/framework/aptos-framework/sources/transaction_validation.spec.move +++ b/aptos-move/framework/aptos-framework/sources/transaction_validation.spec.move @@ -28,6 +28,18 @@ spec aptos_framework::transaction_validation { pragma aborts_if_is_strict; } + spec grant_gas_permission( + master: &signer, + permissioned: &signer, + gas_amount: u64 + ) { + pragma aborts_if_is_partial; + } + + spec revoke_gas_permission(permissioned: &signer) { + pragma aborts_if_is_partial; + } + /// Ensure caller is `aptos_framework`. /// Aborts if TransactionValidation already exists. spec initialize( diff --git a/testsuite/smoke-test/src/lib.rs b/testsuite/smoke-test/src/lib.rs index 2df5effb1d7a3..29f3550bf6628 100644 --- a/testsuite/smoke-test/src/lib.rs +++ b/testsuite/smoke-test/src/lib.rs @@ -35,6 +35,8 @@ mod keyless; #[cfg(test)] mod network; #[cfg(test)] +mod permissioned_delegation; +#[cfg(test)] mod randomness; #[cfg(test)] mod rest_api; diff --git a/testsuite/smoke-test/src/permissioned_delegation.rs b/testsuite/smoke-test/src/permissioned_delegation.rs new file mode 100644 index 0000000000000..84cb7ca68e917 --- /dev/null +++ b/testsuite/smoke-test/src/permissioned_delegation.rs @@ -0,0 +1,145 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +use crate::smoke_test_environment::SwarmBuilder; +use aptos_cached_packages::aptos_stdlib; +use aptos_crypto::SigningKey; +use aptos_forge::Swarm; +use aptos_types::function_info::FunctionInfo; +use move_core_types::account_address::AccountAddress; +use std::sync::Arc; + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_permissioned_delegation() { + let (swarm, mut cli, _faucet) = SwarmBuilder::new_local(1) + .with_aptos() + .build_with_cli(0) + .await; + let mut info = swarm.aptos_public_info(); + + let mut account1 = info + .create_and_fund_user_account(100_000_000_000) + .await + .unwrap(); + let account2 = info.random_account(); + info.create_user_account(account2.public_key()) + .await + .unwrap(); + let account1_private_key = account1.private_key().clone(); + let account1_public_key = account1.public_key().clone(); + let idx = cli.add_account_to_cli(account1_private_key.clone()); + + // Setup permissions: 10 APT allowance, and 0.1 APT gas. + let script = format!( + r#" + script {{ + use std::string::utf8; + use aptos_std::ed25519; + use aptos_framework::coin; + use aptos_framework::permissioned_delegation; + use aptos_framework::primary_fungible_store; + use aptos_framework::transaction_validation; + use aptos_framework::account_abstraction; + fun main(sender: &signer) {{ + coin::migrate_to_fungible_store(sender); + let key = permissioned_delegation::gen_ed25519_key(ed25519::new_unvalidated_public_key_from_bytes(x"{}")); + let permissioned_signer = permissioned_delegation::add_permissioned_handle(sender, key, std::option::none(), {}); + primary_fungible_store::grant_apt_permission(sender, &permissioned_signer, 1000000000); // 10 apt + transaction_validation::grant_gas_permission(sender, &permissioned_signer, 100000000); // 1 apt because that is the max_gas + account_abstraction::add_dispatchable_authentication_function(sender, @aptos_framework, utf8(b"permissioned_delegation"), utf8(b"authenticate")); + }} + }} + "#, + hex::encode(account1_public_key.to_bytes()), + u64::max_value(), + ); + assert_eq!( + Some(true), + cli.run_script(idx, &script).await.unwrap().success + ); + account1.increment_sequence_number(); + + let func_info = FunctionInfo::new( + AccountAddress::ONE, + "permissioned_delegation".to_string(), + "authenticate".to_string(), + ); + account1.set_abstraction_auth( + func_info, + Arc::new(move |x: &[u8]| { + let mut authenticator = vec![]; + authenticator.extend(bcs::to_bytes(&account1_public_key.to_bytes().to_vec()).unwrap()); + authenticator.extend( + bcs::to_bytes( + &account1_private_key + .sign_arbitrary_message(x) + .to_bytes() + .to_vec(), + ) + .unwrap(), + ); + authenticator + }), + ); + + // Transfer 1 APT and 2 APT. + let transfer_txn = account1.sign_aa_transaction_with_transaction_builder( + vec![], + None, + info.transaction_factory() + .payload(aptos_stdlib::aptos_account_fungible_transfer_only( + account2.address(), + 100000000, + )), + ); + info.client().submit_and_wait(&transfer_txn).await.unwrap(); + + // gas permission check failed. + let transfer_txn = account1.sign_aa_transaction_with_transaction_builder( + vec![], + None, + info.transaction_factory() + .payload(aptos_stdlib::aptos_account_fungible_transfer_only( + account2.address(), + 200000000, + )), + ); + assert!(info.client().submit_and_wait(&transfer_txn).await.is_err()); + account1.decrement_sequence_number(); + + let transfer_txn = account1.sign_aa_transaction_with_transaction_builder( + vec![], + None, + info.transaction_factory() + .payload(aptos_stdlib::aptos_account_fungible_transfer_only( + account2.address(), + 200000000, + )) + .max_gas_amount(50000000), + ); + info.client().submit_and_wait(&transfer_txn).await.unwrap(); + + let transfer_txn = account1.sign_aa_transaction_with_transaction_builder( + vec![], + None, + info.transaction_factory() + .payload(aptos_stdlib::aptos_account_fungible_transfer_only( + account2.address(), + 700000001, + )) + .max_gas_amount(50000000), + ); + assert!(info.client().submit_and_wait(&transfer_txn).await.is_err()); + + let transfer_txn = account1.sign_aa_transaction_with_transaction_builder( + vec![], + None, + info.transaction_factory() + .payload(aptos_stdlib::aptos_account_fungible_transfer_only( + account2.address(), + 700000000, + )) + .max_gas_amount(50000000), + ); + info.client().submit_and_wait(&transfer_txn).await.unwrap(); +}