diff --git a/.github/workflows/aptos-framework-test.yaml b/.github/workflows/aptos-framework-test.yaml index d3eba885a1a..aa5881e89a2 100644 --- a/.github/workflows/aptos-framework-test.yaml +++ b/.github/workflows/aptos-framework-test.yaml @@ -30,6 +30,9 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@stable + - name: Install cargo-nextest + uses: taiki-e/install-action@nextest + - name: Setup SSH access run: | mkdir -p ~/.ssh @@ -53,7 +56,7 @@ jobs: run: cargo test --release -p aptos-framework -- --skip prover - name: Run language e2e tests - run: RUST_MIN_STACK=104857600 cargo test run -p language-e2e-testsuite --no-fail-fast + run: RUST_MIN_STACK=104857600 cargo nextest run -p language-e2e-testsuite --no-fail-fast - name: Run move e2e tests - run: RUST_MIN_STACK=104857600 cargo test run -p e2e-move-tests --no-fail-fast + run: RUST_MIN_STACK=104857600 cargo nextest run -p e2e-move-tests --no-fail-fast diff --git a/aptos-move/aptos-vm/src/aptos_vm_viewer.rs b/aptos-move/aptos-vm/src/aptos_vm_viewer.rs index a5ce78285ef..5bad9355228 100644 --- a/aptos-move/aptos-vm/src/aptos_vm_viewer.rs +++ b/aptos-move/aptos-vm/src/aptos_vm_viewer.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. // SPDX-License-Identifier: Apache-2.0 diff --git a/aptos-move/aptos-vm/src/automated_transaction_processor.rs b/aptos-move/aptos-vm/src/automated_transaction_processor.rs index 84d3eb76430..ab0ccaec3e4 100644 --- a/aptos-move/aptos-vm/src/automated_transaction_processor.rs +++ b/aptos-move/aptos-vm/src/automated_transaction_processor.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. // SPDX-License-Identifier: Apache-2.0 diff --git a/aptos-move/aptos-vm/src/automation_registry_transaction_processor.rs b/aptos-move/aptos-vm/src/automation_registry_transaction_processor.rs index 71125163f6d..3341741a43f 100644 --- a/aptos-move/aptos-vm/src/automation_registry_transaction_processor.rs +++ b/aptos-move/aptos-vm/src/automation_registry_transaction_processor.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2025 Supra. // SPDX-License-Identifier: Apache-2.0 diff --git a/aptos-move/e2e-tests/src/executor.rs b/aptos-move/e2e-tests/src/executor.rs index 87da2dbbac4..b483f2c9dbb 100644 --- a/aptos-move/e2e-tests/src/executor.rs +++ b/aptos-move/e2e-tests/src/executor.rs @@ -502,8 +502,9 @@ 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_transaction(&mut self, transaction: Transaction) -> TransactionOutput { - let mut outputs = self.execute_transaction_block(vec![transaction]).unwrap(); + let mut outputs = self.execute_transaction_block(vec![transaction.clone()]).unwrap(); assert_eq!(outputs.len(), 1, "transaction outputs size mismatch"); + println!("transaction execution output: {:#?} : {:#?}", outputs, transaction); let output = outputs.pop().unwrap(); match output.status() { TransactionStatus::Keep(status) => { @@ -522,8 +523,8 @@ impl FakeExecutor { assert_eq!( status, &ExecutionStatus::Success, - "transaction failed with {:?}", - status + "transaction failed with {:?}, {:?}", + status, transaction ); output }, diff --git a/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs b/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs index 931f7b3611a..fdc3fb6e973 100644 --- a/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs +++ b/aptos-move/e2e-testsuite/src/tests/automated_transactions.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. // SPDX-License-Identifier: Apache-2.0 @@ -106,7 +103,7 @@ fn check_automated_transaction_successful_execution() { let payload = aptos_framework_sdk_builder::supra_account_transfer(dest_account.address().clone(), 100); let gas_price = 100; - let max_gas_amount = 100; + let max_gas_amount = 1000; let automation_fee_cap = 100_000; // Register automation task diff --git a/aptos-move/e2e-testsuite/src/tests/automation_registration.rs b/aptos-move/e2e-testsuite/src/tests/automation_registration.rs index 73648ec2e11..38a476c4b37 100644 --- a/aptos-move/e2e-testsuite/src/tests/automation_registration.rs +++ b/aptos-move/e2e-testsuite/src/tests/automation_registration.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. // SPDX-License-Identifier: Apache-2.0 @@ -51,6 +48,8 @@ const HAS_SENDER_ACTIVE_TASK_WITH_ID: &str = "0x1::automation_registry::has_sender_active_task_with_id"; const GET_TASK_IDS: &str = "0x1::automation_registry::get_task_ids"; +const MAX_GAS_AMOUNT_REGISTRATION: u64 = 500_000_000; + struct MultisigAccountData { multisig_address: AccountAddress, owners: Vec, @@ -111,8 +110,8 @@ impl AutomationRegistrationTestContext { fn create_multisig_account_data(executor: &mut FakeExecutor) -> MultisigAccountData { // Prepare multisig_account for system task registration - let multisig_owner1 = executor.create_raw_account_data(1_000_000_000, 0); - let multisig_owner2 = executor.create_raw_account_data(1_000_000_000, 0); + let multisig_owner1 = executor.create_raw_account_data(1_000_000_000_000, 0); + let multisig_owner2 = executor.create_raw_account_data(1_000_000_000_000, 0); executor.add_account_data(&multisig_owner1); executor.add_account_data(&multisig_owner2); let multisig_address = create_multisig_account_address( @@ -129,7 +128,7 @@ impl AutomationRegistrationTestContext { let account_create_txn = multisig_owner1 .account() .transaction() - .max_gas_amount(1_000_000) + .max_gas_amount(5_000_000) .gas_unit_price(100) .payload(create_multisig_payload) .sequence_number(0) @@ -139,7 +138,7 @@ impl AutomationRegistrationTestContext { let transfer_txn = multisig_owner1 .account() .transaction() - .max_gas_amount(1000) + .max_gas_amount(5_000_000) .payload(aptos_stdlib::supra_account_transfer( multisig_address, 10_000_000, @@ -233,6 +232,7 @@ impl AutomationRegistrationTestContext { .payload(automation_txn) .sequence_number(seq_num) .gas_unit_price(1) + .max_gas_amount(MAX_GAS_AMOUNT_REGISTRATION) .sign() } @@ -262,6 +262,7 @@ impl AutomationRegistrationTestContext { .payload(automation_txn) .sequence_number(seq_num) .gas_unit_price(1) + .max_gas_amount(MAX_GAS_AMOUNT_REGISTRATION) .sign() } @@ -286,6 +287,7 @@ impl AutomationRegistrationTestContext { .payload(automation_txn) .sequence_number(seq_num) .gas_unit_price(1) + .max_gas_amount(MAX_GAS_AMOUNT_REGISTRATION) .sign() } @@ -303,6 +305,7 @@ impl AutomationRegistrationTestContext { )) .sequence_number(seq_num) .gas_unit_price(1) + .max_gas_amount(MAX_GAS_AMOUNT_REGISTRATION) .sign() } diff --git a/aptos-move/e2e-testsuite/src/tests/vm_viewer.rs b/aptos-move/e2e-testsuite/src/tests/vm_viewer.rs index 7f5b7ea6a58..10e0b6e33bc 100644 --- a/aptos-move/e2e-testsuite/src/tests/vm_viewer.rs +++ b/aptos-move/e2e-testsuite/src/tests/vm_viewer.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. // SPDX-License-Identifier: Apache-2.0 diff --git a/aptos-move/framework/aptos-stdlib/doc/any.md b/aptos-move/framework/aptos-stdlib/doc/any.md index d8f08373494..5b930d17a03 100644 --- a/aptos-move/framework/aptos-stdlib/doc/any.md +++ b/aptos-move/framework/aptos-stdlib/doc/any.md @@ -11,6 +11,7 @@ - [Function `pack`](#0x1_any_pack) - [Function `unpack`](#0x1_any_unpack) - [Function `type_name`](#0x1_any_type_name) +- [Function `is_empty`](#0x1_any_is_empty) - [Specification](#@Specification_1) - [Function `pack`](#@Specification_1_pack) - [Function `unpack`](#@Specification_1_unpack) @@ -22,6 +23,7 @@ use 0x1::from_bcs; use 0x1::string; use 0x1::type_info; +use 0x1::vector; @@ -191,6 +193,33 @@ Returns the type name of this Any + + + + +## Function `is_empty` + +Returns true if the BCS-encoded data payload is empty. +An Any value with an empty data vector cannot be decoded and should +be treated as invalid by callers. + + +
public fun is_empty(x: &any::Any): bool
+
+ + + +
+Implementation + + +
public fun is_empty(x: &Any): bool {
+    std::vector::is_empty(&x.data)
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-stdlib/doc/copyable_any.md b/aptos-move/framework/aptos-stdlib/doc/copyable_any.md index 7ac120437e6..4803bd75f7e 100644 --- a/aptos-move/framework/aptos-stdlib/doc/copyable_any.md +++ b/aptos-move/framework/aptos-stdlib/doc/copyable_any.md @@ -8,8 +8,10 @@ - [Struct `Any`](#0x1_copyable_any_Any) - [Constants](#@Constants_0) - [Function `pack`](#0x1_copyable_any_pack) +- [Function `new`](#0x1_copyable_any_new) - [Function `unpack`](#0x1_copyable_any_unpack) - [Function `type_name`](#0x1_copyable_any_type_name) +- [Function `is_empty`](#0x1_copyable_any_is_empty) - [Specification](#@Specification_1) - [Function `pack`](#@Specification_1_pack) - [Function `unpack`](#@Specification_1_unpack) @@ -21,6 +23,7 @@ use 0x1::from_bcs; use 0x1::string; use 0x1::type_info; +use 0x1::vector; @@ -101,6 +104,39 @@ also required from T. + + + + +## Function `new` + +Construct an Any directly from a pre-known type-name string and +already-BCS-encoded data bytes. Use this only when the type name is +obtained from a trusted source (e.g. type_info::type_name<T>() or a +constant produced by the same) and the data bytes are the BCS encoding +of a value of that type. Prefer pack<T> whenever the concrete type +is statically known at the call site, as it is safer and self-validating. + +This constructor exists primarily for genesis and governance scripts that +receive pre-serialised values from the Rust layer. + + +
public fun new(type_name: string::String, data: vector<u8>): copyable_any::Any
+
+ + + +
+Implementation + + +
public fun new(type_name: String, data: vector<u8>): Any {
+    Any { type_name, data }
+}
+
+ + +
@@ -152,6 +188,33 @@ Returns the type name of this Any + + + + +## Function `is_empty` + +Returns true if the BCS-encoded data payload is empty. +An Any value with an empty data vector cannot be decoded and should +be treated as invalid by callers. + + +
public fun is_empty(x: &copyable_any::Any): bool
+
+ + + +
+Implementation + + +
public fun is_empty(x: &Any): bool {
+    std::vector::is_empty(&x.data)
+}
+
+ + +
diff --git a/aptos-move/framework/aptos-stdlib/sources/any.move b/aptos-move/framework/aptos-stdlib/sources/any.move index 88b9f4dd780..a275828d4cf 100644 --- a/aptos-move/framework/aptos-stdlib/sources/any.move +++ b/aptos-move/framework/aptos-stdlib/sources/any.move @@ -54,6 +54,13 @@ module aptos_std::any { &x.type_name } + /// Returns true if the BCS-encoded data payload is empty. + /// An Any value with an empty data vector cannot be decoded and should + /// be treated as invalid by callers. + public fun is_empty(x: &Any): bool { + std::vector::is_empty(&x.data) + } + #[test_only] struct S has store, drop { x: u64 } diff --git a/aptos-move/framework/aptos-stdlib/sources/copyable_any.move b/aptos-move/framework/aptos-stdlib/sources/copyable_any.move index b12303a3f92..dc288f0faec 100644 --- a/aptos-move/framework/aptos-stdlib/sources/copyable_any.move +++ b/aptos-move/framework/aptos-stdlib/sources/copyable_any.move @@ -23,6 +23,19 @@ module aptos_std::copyable_any { } } + /// Construct an `Any` directly from a pre-known type-name string and + /// already-BCS-encoded data bytes. Use this only when the type name is + /// obtained from a trusted source (e.g. `type_info::type_name()` or a + /// constant produced by the same) and the data bytes are the BCS encoding + /// of a value of that type. Prefer `pack` whenever the concrete type + /// is statically known at the call site, as it is safer and self-validating. + /// + /// This constructor exists primarily for genesis and governance scripts that + /// receive pre-serialised values from the Rust layer. + public fun new(type_name: String, data: vector): Any { + Any { type_name, data } + } + /// Unpack a value from the `Any` representation. This aborts if the value has not the expected type `T`. public fun unpack(x: Any): T { assert!(type_info::type_name() == x.type_name, error::invalid_argument(ETYPE_MISMATCH)); @@ -34,6 +47,13 @@ module aptos_std::copyable_any { &x.type_name } + /// Returns true if the BCS-encoded data payload is empty. + /// An Any value with an empty data vector cannot be decoded and should + /// be treated as invalid by callers. + public fun is_empty(x: &Any): bool { + std::vector::is_empty(&x.data) + } + #[test_only] struct S has store, drop, copy { x: u64 } diff --git a/aptos-move/framework/src/natives/automation_registry_callbacks.rs b/aptos-move/framework/src/natives/automation_registry_callbacks.rs index 462756d9fd1..bc2aeb2a13d 100644 --- a/aptos-move/framework/src/natives/automation_registry_callbacks.rs +++ b/aptos-move/framework/src/natives/automation_registry_callbacks.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2025 Supra. // SPDX-License-Identifier: Apache-2.0 use aptos_native_interface::{SafeNativeBuilder, SafeNativeContext, SafeNativeResult}; diff --git a/aptos-move/framework/src/natives/cryptography/bls12381_bulletproofs.rs b/aptos-move/framework/src/natives/cryptography/bls12381_bulletproofs.rs index 01530e3accc..7839d844271 100644 --- a/aptos-move/framework/src/natives/cryptography/bls12381_bulletproofs.rs +++ b/aptos-move/framework/src/natives/cryptography/bls12381_bulletproofs.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. use crate::natives::cryptography::bulletproofs::abort_codes; diff --git a/aptos-move/framework/src/natives/cryptography/bls12381_scalar.rs b/aptos-move/framework/src/natives/cryptography/bls12381_scalar.rs index 1a544baa646..831cbedc52e 100644 --- a/aptos-move/framework/src/natives/cryptography/bls12381_scalar.rs +++ b/aptos-move/framework/src/natives/cryptography/bls12381_scalar.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. use aptos_gas_schedule::gas_params::natives::aptos_framework::{ diff --git a/aptos-move/framework/src/natives/cryptography/eth_trie.rs b/aptos-move/framework/src/natives/cryptography/eth_trie.rs index 6dc32154c90..bafc3006fb2 100644 --- a/aptos-move/framework/src/natives/cryptography/eth_trie.rs +++ b/aptos-move/framework/src/natives/cryptography/eth_trie.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. use aptos_gas_schedule::gas_params::natives::aptos_framework::{ diff --git a/aptos-move/framework/src/natives/rlp.rs b/aptos-move/framework/src/natives/rlp.rs index 413bb9cf867..9f650350766 100644 --- a/aptos-move/framework/src/natives/rlp.rs +++ b/aptos-move/framework/src/natives/rlp.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. use aptos_gas_schedule::gas_params::natives::aptos_framework::{ diff --git a/aptos-move/framework/supra-framework/doc/evm_config.md b/aptos-move/framework/supra-framework/doc/evm_config.md index 266294bb013..9644b3bb39b 100644 --- a/aptos-move/framework/supra-framework/doc/evm_config.md +++ b/aptos-move/framework/supra-framework/doc/evm_config.md @@ -5,33 +5,41 @@ -- [Resource `EvmConfig`](#0x1_evm_config_EvmConfig) +- [Resource `EvmContractsDetails`](#0x1_evm_config_EvmContractsDetails) +- [Resource `EvmScalarConfig`](#0x1_evm_config_EvmScalarConfig) - [Constants](#@Constants_0) - [Function `initialize`](#0x1_evm_config_initialize) -- [Function `set_for_next_epoch`](#0x1_evm_config_set_for_next_epoch) +- [Function `upsert_evm_contract_details_for_next_epoch`](#0x1_evm_config_upsert_evm_contract_details_for_next_epoch) +- [Function `upsert_config_for_next_epoch`](#0x1_evm_config_upsert_config_for_next_epoch) +- [Function `get_contract_value`](#0x1_evm_config_get_contract_value) +- [Function `get_scalar_config_value`](#0x1_evm_config_get_scalar_config_value) +- [Function `validate_scalar_config`](#0x1_evm_config_validate_scalar_config) +- [Function `is_valid_evm_address`](#0x1_evm_config_is_valid_evm_address) - [Function `on_new_epoch`](#0x1_evm_config_on_new_epoch) -- [Specification](#@Specification_1) - - [Function `initialize`](#@Specification_1_initialize) - - [Function `set_for_next_epoch`](#@Specification_1_set_for_next_epoch) - - [Function `on_new_epoch`](#@Specification_1_on_new_epoch) -
use 0x1::config_buffer;
+
use 0x1::bcs;
+use 0x1::config_buffer;
 use 0x1::error;
+use 0x1::event;
+use 0x1::option;
+use 0x1::signer;
+use 0x1::simple_map;
+use 0x1::string;
 use 0x1::system_addresses;
 use 0x1::vector;
 
- + -## Resource `EvmConfig` +## Resource `EvmContractsDetails` -The struct stores the on-chain EVM configuration. -
struct EvmConfig has drop, store, key
+
#[event]
+struct EvmContractsDetails has copy, drop, store, key
 
@@ -42,7 +50,35 @@ The struct stores the on-chain EVM configuration.
-config: vector<u8> +details: simple_map::SimpleMap<string::String, address> +
+
+ +
+
+ + + + + + +## Resource `EvmScalarConfig` + + + +
#[event]
+struct EvmScalarConfig has copy, drop, store, key
+
+ + + +
+Fields + + +
+
+config: simple_map::SimpleMap<string::String, u128>
@@ -57,12 +93,132 @@ The struct stores the on-chain EVM configuration. ## Constants - + + +Requested key does not exist in the map + + +
const EKEY_NOT_FOUND: u64 = 4;
+
+ + + + + +Supra EVM Gas Estimate Margin + + +
const CONFIG_KEY_EVM_GAS_ESTIMATE_MARGIN: vector<u8> = [101, 118, 109, 95, 103, 97, 115, 95, 101, 115, 116, 105, 109, 97, 116, 101, 95, 109, 97, 114, 103, 105, 110];
+
+ + + + + +Well-known config key for the EVM gas normalisation denominator. +This u64 value is used to scale EVM gas units into Supra gas units. + + +
const CONFIG_KEY_EVM_GAS_NORMALIZATION_DENOM: vector<u8> = [101, 118, 109, 95, 103, 97, 115, 95, 110, 111, 114, 109, 97, 108, 105, 122, 97, 116, 105, 111, 110, 95, 100, 101, 110, 111, 109];
+
+ + + + + +Empty keys/values to update config. + + +
const EEMPTY_DATA: u64 = 1;
+
+ + + + + +gas used ratio cannot exceed decimal precision + + +
const EGAS_USED_RATIO_CANNOT_EXCEED_DECIMAL_PRECISION: u64 = 7;
+
+ + + + + +Invalid EVM address, valid EVM address must fit in 20 bytes + + +
const EINVALID_EVM_ADDRESS: u64 = 3;
+
+ + + + + +Input keys and values should have the same amount of data + + +
const EKEYS_VALUES_MISMATCH: u64 = 2;
+
-The provided on chain config bytes are empty or invalid -
const EINVALID_CONFIG: u64 = 1;
+
+
+Required Key is missing or value type is incorrect for a key
+
+
+
const EMISSING_KEY_OR_INCORRECT_VAL_TYPE: u64 = 5;
+
+ + + + + +Resource already exist at address + + +
const ERESOURCE_ALREADY_EXISTS: u64 = 6;
+
+ + + + + +Evm and Move address length + + +
const EVM_ADDRESS_BYTE_LENGTH: u64 = 20;
+
+ + + + + + + +
const MOVE_ADDRESS_BYTE_LENGTH: u64 = 32;
+
+ + + + + +Supra EVM Gas Used Ratio + + +
const SUPRA_EVM_GAS_USED_RATIO: vector<u8> = [101, 118, 109, 95, 103, 97, 115, 95, 117, 115, 101, 100, 95, 114, 97, 116, 105, 111];
+
+ + + + + +DECIMAL_PRECISION + + +
const SUPRA_EVM_GAS_USED_RATIO_DECIMAL_PRECISION: vector<u8> = [101, 118, 109, 95, 103, 97, 115, 95, 117, 115, 101, 100, 95, 114, 97, 116, 105, 111, 95, 100, 101, 99, 105, 109, 97, 108, 95, 112, 114, 101, 99, 105, 115, 105, 111, 110];
 
@@ -71,10 +227,14 @@ The provided on chain config bytes are empty or invalid ## Function `initialize` -Publishes the EvmConfig config. +Publishes both the EVM contract address map and the scalar config map. +contract_keys/contract_values must be the same length and every address +must be a valid 20-byte EVM address (upper 12 bytes of the 32-byte Move +address must be zero). config_keys/config_values must be the same +length and must include all required keys (e.g. evm_gas_normalization_denom). -
public(friend) fun initialize(supra_framework: &signer, config: vector<u8>)
+
public(friend) fun initialize(supra_framework: &signer, contract_keys: vector<string::String>, contract_values: vector<address>, config_keys: vector<string::String>, config_values: vector<u128>)
 
@@ -83,10 +243,45 @@ Publishes the EvmConfig config. Implementation -
public(friend) fun initialize(supra_framework: &signer, config: vector<u8>) {
+
public(friend) fun initialize(
+    supra_framework: &signer,
+    contract_keys: vector<String>,
+    contract_values: vector<address>,
+    config_keys: vector<String>,
+    config_values: vector<u128>
+) {
     system_addresses::assert_supra_framework(supra_framework);
-    assert!(!vector::is_empty(&config), error::invalid_argument(EINVALID_CONFIG));
-    move_to(supra_framework, EvmConfig { config });
+    assert!(!vector::is_empty(&contract_keys), error::invalid_argument(EEMPTY_DATA));
+    assert!(!vector::is_empty(&config_keys), error::invalid_argument(EEMPTY_DATA));
+    let supra_framework_addr = signer::address_of(supra_framework);
+    assert!(!exists<EvmContractsDetails>(supra_framework_addr),error::invalid_state(ERESOURCE_ALREADY_EXISTS));
+    assert!(!exists<EvmScalarConfig>(supra_framework_addr),error::invalid_state(ERESOURCE_ALREADY_EXISTS));
+    assert!(
+        vector::length(&contract_keys) == vector::length(&contract_values),
+        error::invalid_argument(EKEYS_VALUES_MISMATCH)
+    );
+    assert!(
+        vector::length(&config_keys) == vector::length(&config_values),
+        error::invalid_argument(EKEYS_VALUES_MISMATCH)
+    );
+
+    // Check that no contract value is invalid EVM address
+    let all_valid_evm_address = vector::all(&contract_values,
+    |v|{ is_valid_evm_address(v) });
+    assert!(all_valid_evm_address, error::invalid_argument(EINVALID_EVM_ADDRESS));
+
+    let contract_details = EvmContractsDetails {
+        details: simple_map::new_from(contract_keys, contract_values)
+    };
+    move_to(supra_framework, contract_details);
+    event::emit(contract_details);
+
+    let evm_config = EvmScalarConfig {
+        config: simple_map::new_from(config_keys, config_values)
+    };
+    validate_scalar_config(&evm_config);
+    move_to(supra_framework, evm_config);
+    event::emit(evm_config);
 }
 
@@ -94,19 +289,21 @@ Publishes the EvmConfig config.
- + -## Function `set_for_next_epoch` +## Function `upsert_evm_contract_details_for_next_epoch` -This can be called by on-chain governance to update on-chain evm configs for the next epoch. +This can be called by on-chain governance to update on-chain evm contract +details for the next epoch. Example usage: ``` -supra_framework::evm_config::set_for_next_epoch(&framework_signer, some_config_bytes); +supra_framework::evm_config::upsert_evm_contract_details_for_next_epoch( +&framework_signer, vector["contract1_name"], vector[contract1_address]); supra_framework::supra_governance::reconfigure(&framework_signer); ``` -
public fun set_for_next_epoch(account: &signer, config: vector<u8>)
+
public fun upsert_evm_contract_details_for_next_epoch(account: &signer, keys: vector<string::String>, values: vector<address>)
 
@@ -115,10 +312,30 @@ supra_framework::supra_governance::reconfigure(&framework_signer); Implementation -
public fun set_for_next_epoch(account: &signer, config: vector<u8>) {
+
public fun upsert_evm_contract_details_for_next_epoch(
+    account: &signer,
+    keys: vector<String>,
+    values: vector<address>
+) acquires EvmContractsDetails {
     system_addresses::assert_supra_framework(account);
-    assert!(!vector::is_empty(&config), error::invalid_argument(EINVALID_CONFIG));
-    std::config_buffer::upsert<EvmConfig>(EvmConfig {config});
+    assert!(!vector::is_empty(&keys), error::invalid_argument(EEMPTY_DATA));
+    assert!(
+        vector::length(&keys) == vector::length(&values),
+        error::invalid_argument(EKEYS_VALUES_MISMATCH)
+    );
+    let all_valid_evm_address = vector::all(&values, |v| { is_valid_evm_address(v) });
+    assert!(all_valid_evm_address, error::invalid_argument(EINVALID_EVM_ADDRESS));
+    if (!exists<EvmContractsDetails>(@supra_framework)) {
+        std::config_buffer::upsert<EvmContractsDetails>(
+            EvmContractsDetails { details: simple_map::new_from(keys, values) }
+        );
+        return
+    };
+    let updated_config = *borrow_global<EvmContractsDetails>(@supra_framework);
+    vector::zip(keys, values, |key, value| {
+        simple_map::upsert(&mut updated_config.details, key, value);
+    });
+    std::config_buffer::upsert<EvmContractsDetails>(updated_config);
 }
 
@@ -126,16 +343,21 @@ supra_framework::supra_governance::reconfigure(&framework_signer); - + -## Function `on_new_epoch` +## Function `upsert_config_for_next_epoch` -Only used in reconfigurations to apply the pending EvmConfig in buffer, if there is any. -If supra_framework has a EvmConfig, then update the new config to supra_framework. -Otherwise, move the new config to supra_framework. +This can be called by on-chain governance to update on-chain evm config +for the next epoch. Values are plain u128 scalars. +Example usage: +``` +supra_framework::evm_config::upsert_config_for_next_epoch( +&framework_signer, vector["config_key"], vector[new_value]); +supra_framework::supra_governance::reconfigure(&framework_signer); +``` -
public(friend) fun on_new_epoch(framework: &signer)
+
public fun upsert_config_for_next_epoch(account: &signer, keys: vector<string::String>, values: vector<u128>)
 
@@ -144,16 +366,35 @@ Otherwise, move the new config to supra_framework. Implementation -
public(friend) fun on_new_epoch(framework: &signer) acquires EvmConfig {
-    system_addresses::assert_supra_framework(framework);
-    if (config_buffer::does_exist<EvmConfig>()) {
-        let new_config = config_buffer::extract<EvmConfig>();
-        if (exists<EvmConfig>(@supra_framework)) {
-            *borrow_global_mut<EvmConfig>(@supra_framework) = new_config;
-        } else {
-            move_to(framework, new_config);
-        };
-    }
+
public fun upsert_config_for_next_epoch(
+    account: &signer,
+    keys: vector<String>,
+    values: vector<u128>
+) acquires EvmScalarConfig {
+    system_addresses::assert_supra_framework(account);
+    assert!(!vector::is_empty(&keys), error::invalid_argument(EEMPTY_DATA));
+    assert!(
+        vector::length(&keys) == vector::length(&values),
+        error::invalid_argument(EKEYS_VALUES_MISMATCH)
+    );
+    if (!exists<EvmScalarConfig>(@supra_framework)) {
+        let evm_config =
+            EvmScalarConfig { config: simple_map::new_from(keys, values) };
+        // Config did not exist earlier, so validate the config for
+        // presence of required keys and value type match
+        validate_scalar_config(&evm_config);
+        std::config_buffer::upsert<EvmScalarConfig>(evm_config);
+        return;
+    };
+    // Copy existing config
+    let updated_config = *borrow_global<EvmScalarConfig>(@supra_framework);
+    // We are never removing existing keys so by induction if all required
+    // keys are present in config during initialization
+    // they will be there later as well, so no need to validate here
+    vector::zip(keys, values, |key, value| {
+        simple_map::upsert(&mut updated_config.config, key, value);
+    });
+    std::config_buffer::upsert<EvmScalarConfig>(updated_config);
 }
 
@@ -161,53 +402,169 @@ Otherwise, move the new config to supra_framework. - + -## Specification +## Function `get_contract_value` +Returns the contract address stored under key in the EvmContractsDetails map. +Aborts with EKEY_NOT_FOUND if the resource has not been initialised or the key +is not present in the map. -
pragma verify = true;
-pragma aborts_if_is_strict;
+
#[view]
+public fun get_contract_value(key: string::String): address
 
- +
+Implementation + + +
public fun get_contract_value(key: String): address acquires EvmContractsDetails {
+    assert!(exists<EvmContractsDetails>(@supra_framework), error::not_found(EKEY_NOT_FOUND));
+    let details = borrow_global<EvmContractsDetails>(@supra_framework);
+    assert!(
+        simple_map::contains_key(&details.details, &key),
+        error::not_found(EKEY_NOT_FOUND)
+    );
+    *simple_map::borrow(&details.details, &key)
+}
+
+ -### Function `initialize` +
-
public(friend) fun initialize(supra_framework: &signer, config: vector<u8>)
+
+
+## Function `get_scalar_config_value`
+
+Returns the u128 value stored under key in the EvmScalarConfig map.
+Aborts with EKEY_NOT_FOUND if the resource has not been initialised or the key
+is not present in the map.
+
+
+
#[view]
+public fun get_scalar_config_value(key: string::String): u128
 
+
+Implementation -
pragma aborts_if_is_strict = false;
+
+
public fun get_scalar_config_value(key: String): u128 acquires EvmScalarConfig {
+    assert!(exists<EvmScalarConfig>(@supra_framework), error::not_found(EKEY_NOT_FOUND));
+    let config = borrow_global<EvmScalarConfig>(@supra_framework);
+    assert!(
+        simple_map::contains_key(&config.config, &key),
+        error::not_found(EKEY_NOT_FOUND)
+    );
+    *simple_map::borrow(&config.config, &key)
+}
 
- +
+ + + +## Function `validate_scalar_config` -### Function `set_for_next_epoch` -
public fun set_for_next_epoch(account: &signer, config: vector<u8>)
+
fun validate_scalar_config(evm_config: &evm_config::EvmScalarConfig)
 
+
+Implementation + -
include config_buffer::SetForNextEpochAbortsIf;
+
fun validate_scalar_config(evm_config: &EvmScalarConfig) {
+    let required_keys = vector[string::utf8(CONFIG_KEY_EVM_GAS_NORMALIZATION_DENOM),
+                               string::utf8(CONFIG_KEY_EVM_GAS_ESTIMATE_MARGIN),
+                               string::utf8(SUPRA_EVM_GAS_USED_RATIO),
+                               string::utf8(SUPRA_EVM_GAS_USED_RATIO_DECIMAL_PRECISION)];
+    // With u128 as the value type, the Move type system prevents type mismatches at
+    // compile time. The only meaningful runtime check is key presence.
+    vector::for_each_reverse(required_keys, |rk| {
+        assert!(simple_map::contains_key(&evm_config.config, &rk),
+            error::invalid_argument(EMISSING_KEY_OR_INCORRECT_VAL_TYPE)
+        );
+        assert!(*simple_map::borrow(&evm_config.config, &rk) > 0u128, error::invalid_argument(EMISSING_KEY_OR_INCORRECT_VAL_TYPE));
+    });
+    let gas_used_ratio = *simple_map::borrow(&evm_config.config, &string::utf8(SUPRA_EVM_GAS_USED_RATIO));
+    let gas_used_ratio_decimal_precision = *simple_map::borrow(&evm_config.config, &string::utf8(SUPRA_EVM_GAS_USED_RATIO_DECIMAL_PRECISION));
+    assert!(gas_used_ratio <= gas_used_ratio_decimal_precision, error::invalid_argument(EGAS_USED_RATIO_CANNOT_EXCEED_DECIMAL_PRECISION));
+}
 
- +
+ + + +## Function `is_valid_evm_address` + + + +
fun is_valid_evm_address(addr: &address): bool
+
-### Function `on_new_epoch` + + +
+Implementation + + +
fun is_valid_evm_address(addr: &address): bool {
+    // BCS serialises a Move `address` as a raw fixed-size 32-byte array in
+    // big-endian order (most-significant byte first).  An EVM address is only
+    // 20 bytes wide, so when a 20-byte EVM address is stored in a 32-byte Move
+    // address the EVM bytes occupy the last 20 positions (indices 12-31) and the
+    // leading 12 bytes (indices 0-11) must all be zero.
+    // We iterate only over that prefix and bail immediately on the first non-zero
+    // byte to avoid unnecessary work.
+    let evm_addr_bytes = bcs::to_bytes(addr);
+
+    // Sanity check: BCS encoding of an address is always 32 bytes. If somehow
+    // the length differs, the address cannot be valid.
+    if (vector::length(&evm_addr_bytes) != MOVE_ADDRESS_BYTE_LENGTH) {
+        return false
+    };
+
+    // Check that all 12 high-order prefix bytes are zero.  A non-zero byte here
+    // means the value cannot fit in 20 bytes and is therefore not a valid EVM address.
+    let i = 0u64;
+    let addr_length_diff = MOVE_ADDRESS_BYTE_LENGTH - EVM_ADDRESS_BYTE_LENGTH;
+    while (i < addr_length_diff) {
+        if (*vector::borrow(&evm_addr_bytes, i) != 0u8) {
+            // Non-zero prefix byte found - not a valid 20-byte EVM address.
+            return false
+        };
+        i = i + 1;
+    };
+
+    true
+}
+
+ + + +
+ + + +## Function `on_new_epoch` + +Only used in reconfigurations to apply the pending configs in buffer, if any. +If supra_framework already holds the resource, overwrite it; otherwise move it in.
public(friend) fun on_new_epoch(framework: &signer)
@@ -215,11 +572,40 @@ Otherwise, move the new config to supra_framework.
 
 
 
+
+Implementation -
requires @supra_framework == std::signer::address_of(framework);
-include config_buffer::OnNewEpochRequirement<EvmConfig>;
-aborts_if false;
+
+
public(friend) fun on_new_epoch(framework: &signer) acquires EvmScalarConfig, EvmContractsDetails {
+    system_addresses::assert_supra_framework(framework);
+    if (config_buffer::does_exist<EvmContractsDetails>()) {
+        //TODO: change to extract_v2 when extract_v2 is merged and available
+        let new_config = config_buffer::extract<EvmContractsDetails>();
+        if (!exists<EvmContractsDetails>(@supra_framework)) {
+            move_to(framework, new_config);
+        } else {
+            let old_config = borrow_global_mut<EvmContractsDetails>(@supra_framework);
+            *old_config = new_config;
+        };
+        event::emit(new_config)
+    };
+    if (config_buffer::does_exist<EvmScalarConfig>()) {
+        // change to extract_v2 when it is available
+        let new_config = config_buffer::extract<EvmScalarConfig>();
+        if (!exists<EvmScalarConfig>(@supra_framework)) {
+            move_to(framework, new_config);
+        } else {
+            let old_config = borrow_global_mut<EvmScalarConfig>(@supra_framework);
+            *old_config = new_config;
+        };
+        event::emit(new_config)
+    };
+}
 
+ +
+ + [move-book]: https://aptos.dev/move/book/SUMMARY diff --git a/aptos-move/framework/supra-framework/doc/genesis.md b/aptos-move/framework/supra-framework/doc/genesis.md index 68589c74226..a3a7417e932 100644 --- a/aptos-move/framework/supra-framework/doc/genesis.md +++ b/aptos-move/framework/supra-framework/doc/genesis.md @@ -19,6 +19,7 @@ - [Function `initialize_supra_native_automation_v2`](#0x1_genesis_initialize_supra_native_automation_v2) - [Function `initialize_core_resources_and_supra_coin`](#0x1_genesis_initialize_core_resources_and_supra_coin) - [Function `initialize_evm_genesis_config`](#0x1_genesis_initialize_evm_genesis_config) +- [Function `initialize_evm_config`](#0x1_genesis_initialize_evm_config) - [Function `initialize_leader_ban_registry_config`](#0x1_genesis_initialize_leader_ban_registry_config) - [Function `create_accounts`](#0x1_genesis_create_accounts) - [Function `create_account`](#0x1_genesis_create_account) @@ -62,6 +63,7 @@ use 0x1::create_signer; use 0x1::dkg_config; use 0x1::error; +use 0x1::evm_config; use 0x1::evm_genesis_config; use 0x1::execution_config; use 0x1::features; @@ -845,6 +847,37 @@ Initialize the EVM genesis config. + + + + +## Function `initialize_evm_config` + +Initialize the EVM config. + + +
fun initialize_evm_config(supra_framework: &signer, contract_names: vector<string::String>, contract_addresses: vector<address>, config_keys: vector<string::String>, config_values: vector<u128>)
+
+ + + +
+Implementation + + +
fun initialize_evm_config(
+    supra_framework: &signer,
+    contract_names: vector<String>,
+    contract_addresses: vector<address>,
+    config_keys: vector<String>,
+    config_values: vector<u128>
+) {
+    evm_config::initialize(supra_framework, contract_names, contract_addresses, config_keys, config_values);
+}
+
+ + +
diff --git a/aptos-move/framework/supra-framework/doc/overview.md b/aptos-move/framework/supra-framework/doc/overview.md index 0bf36b9ced1..f07a02a84ae 100644 --- a/aptos-move/framework/supra-framework/doc/overview.md +++ b/aptos-move/framework/supra-framework/doc/overview.md @@ -31,6 +31,7 @@ This is the reference documentation of the Supra framework. - [`0x1::dkg_committee`](dkg_committee.md#0x1_dkg_committee) - [`0x1::dkg_config`](dkg_config.md#0x1_dkg_config) - [`0x1::event`](event.md#0x1_event) +- [`0x1::evm_config`](evm_config.md#0x1_evm_config) - [`0x1::evm_genesis_config`](evm_genesis_config.md#0x1_evm_genesis_config) - [`0x1::execution_config`](execution_config.md#0x1_execution_config) - [`0x1::function_info`](function_info.md#0x1_function_info) diff --git a/aptos-move/framework/supra-framework/doc/reconfiguration_with_dkg.md b/aptos-move/framework/supra-framework/doc/reconfiguration_with_dkg.md index d18866c7486..f894b2ba518 100644 --- a/aptos-move/framework/supra-framework/doc/reconfiguration_with_dkg.md +++ b/aptos-move/framework/supra-framework/doc/reconfiguration_with_dkg.md @@ -20,6 +20,7 @@ Reconfiguration with DKG helper functions. use 0x1::consensus_config; use 0x1::dkg_committee; use 0x1::dkg_config; +use 0x1::evm_config; use 0x1::evm_genesis_config; use 0x1::execution_config; use 0x1::features; @@ -177,6 +178,7 @@ Run the default reconfiguration to enter the new epoch. randomness_api_v0_config::on_new_epoch(framework); evm_genesis_config::on_new_epoch(framework); dkg_config::on_new_epoch(framework); + evm_config::on_new_epoch(framework); reconfiguration::reconfigure(); }
diff --git a/aptos-move/framework/supra-framework/sources/configs/config_buffer.move b/aptos-move/framework/supra-framework/sources/configs/config_buffer.move index 6e0019d13d0..4b97ec1cabe 100644 --- a/aptos-move/framework/supra-framework/sources/configs/config_buffer.move +++ b/aptos-move/framework/supra-framework/sources/configs/config_buffer.move @@ -19,6 +19,7 @@ module supra_framework::config_buffer { use supra_framework::system_addresses; + friend supra_framework::evm_config; friend supra_framework::evm_genesis_config; friend supra_framework::consensus_config; friend supra_framework::execution_config; diff --git a/aptos-move/framework/supra-framework/sources/configs/evm_config.move b/aptos-move/framework/supra-framework/sources/configs/evm_config.move new file mode 100644 index 00000000000..a17464e9120 --- /dev/null +++ b/aptos-move/framework/supra-framework/sources/configs/evm_config.move @@ -0,0 +1,484 @@ +module supra_framework::evm_config { + + use std::error; + use std::signer; + use std::string::{Self,String}; + use std::vector; + use aptos_std::simple_map; + use aptos_std::simple_map::SimpleMap; + use aptos_std::bcs; + use supra_framework::config_buffer; + use supra_framework::event; + use supra_framework::system_addresses; + + friend supra_framework::genesis; + friend supra_framework::reconfiguration_with_dkg; + + /// Empty keys/values to update config. + const EEMPTY_DATA: u64 = 1; + + /// Input keys and values should have the same amount of data + const EKEYS_VALUES_MISMATCH: u64 = 2; + + /// Invalid EVM address, valid EVM address must fit in 20 bytes + const EINVALID_EVM_ADDRESS: u64 = 3; + + /// Requested key does not exist in the map + const EKEY_NOT_FOUND: u64 = 4; + + /// Required Key is missing or value type is incorrect for a key + const EMISSING_KEY_OR_INCORRECT_VAL_TYPE: u64 = 5; + + /// Resource already exist at address + const ERESOURCE_ALREADY_EXISTS: u64 = 6; + + /// gas used ratio cannot exceed decimal precision + const EGAS_USED_RATIO_CANNOT_EXCEED_DECIMAL_PRECISION: u64 = 7; + + /// Well-known config key for the EVM gas normalisation denominator. + /// This u64 value is used to scale EVM gas units into Supra gas units. + const CONFIG_KEY_EVM_GAS_NORMALIZATION_DENOM: vector = b"evm_gas_normalization_denom"; + + /// Supra EVM Gas Estimate Margin + const CONFIG_KEY_EVM_GAS_ESTIMATE_MARGIN: vector = b"evm_gas_estimate_margin"; + + /// Supra EVM Gas Used Ratio + const SUPRA_EVM_GAS_USED_RATIO : vector = b"evm_gas_used_ratio"; + + /// DECIMAL_PRECISION + const SUPRA_EVM_GAS_USED_RATIO_DECIMAL_PRECISION: vector = b"evm_gas_used_ratio_decimal_precision"; + + /// Evm and Move address length + const EVM_ADDRESS_BYTE_LENGTH : u64 = 20; + const MOVE_ADDRESS_BYTE_LENGTH: u64 = 32; + + #[event] + struct EvmContractsDetails has key, copy, store, drop { + details: SimpleMap, + } + + /// key-value map of EVM scalar config parameters. + /// Values are plain u128 integers; the Move type system enforces type safety + /// at compile time, eliminating the need for self-describing Any wrappers. + #[event] + struct EvmScalarConfig has key, copy, store, drop { + config: SimpleMap + } + + /// Publishes both the EVM contract address map and the scalar config map. + /// `contract_keys`/`contract_values` must be the same length and every address + /// must be a valid 20-byte EVM address (upper 12 bytes of the 32-byte Move + /// address must be zero). `config_keys`/`config_values` must be the same + /// length and must include all required keys (e.g. evm_gas_normalization_denom). + public(friend) fun initialize( + supra_framework: &signer, + contract_keys: vector, + contract_values: vector
, + config_keys: vector, + config_values: vector + ) { + system_addresses::assert_supra_framework(supra_framework); + assert!(!vector::is_empty(&contract_keys), error::invalid_argument(EEMPTY_DATA)); + assert!(!vector::is_empty(&config_keys), error::invalid_argument(EEMPTY_DATA)); + let supra_framework_addr = signer::address_of(supra_framework); + assert!(!exists(supra_framework_addr),error::invalid_state(ERESOURCE_ALREADY_EXISTS)); + assert!(!exists(supra_framework_addr),error::invalid_state(ERESOURCE_ALREADY_EXISTS)); + assert!( + vector::length(&contract_keys) == vector::length(&contract_values), + error::invalid_argument(EKEYS_VALUES_MISMATCH) + ); + assert!( + vector::length(&config_keys) == vector::length(&config_values), + error::invalid_argument(EKEYS_VALUES_MISMATCH) + ); + + // Check that no contract value is invalid EVM address + let all_valid_evm_address = vector::all(&contract_values, + |v|{ is_valid_evm_address(v) }); + assert!(all_valid_evm_address, error::invalid_argument(EINVALID_EVM_ADDRESS)); + + let contract_details = EvmContractsDetails { + details: simple_map::new_from(contract_keys, contract_values) + }; + move_to(supra_framework, contract_details); + event::emit(contract_details); + + let evm_config = EvmScalarConfig { + config: simple_map::new_from(config_keys, config_values) + }; + validate_scalar_config(&evm_config); + move_to(supra_framework, evm_config); + event::emit(evm_config); + } + + /// This can be called by on-chain governance to update on-chain evm contract + /// details for the next epoch. + /// Example usage: + /// ``` + /// supra_framework::evm_config::upsert_evm_contract_details_for_next_epoch( + /// &framework_signer, vector["contract1_name"], vector[contract1_address]); + /// supra_framework::supra_governance::reconfigure(&framework_signer); + /// ``` + public fun upsert_evm_contract_details_for_next_epoch( + account: &signer, + keys: vector, + values: vector
+ ) acquires EvmContractsDetails { + system_addresses::assert_supra_framework(account); + assert!(!vector::is_empty(&keys), error::invalid_argument(EEMPTY_DATA)); + assert!( + vector::length(&keys) == vector::length(&values), + error::invalid_argument(EKEYS_VALUES_MISMATCH) + ); + let all_valid_evm_address = vector::all(&values, |v| { is_valid_evm_address(v) }); + assert!(all_valid_evm_address, error::invalid_argument(EINVALID_EVM_ADDRESS)); + if (!exists(@supra_framework)) { + std::config_buffer::upsert( + EvmContractsDetails { details: simple_map::new_from(keys, values) } + ); + return + }; + let updated_config = *borrow_global(@supra_framework); + vector::zip(keys, values, |key, value| { + simple_map::upsert(&mut updated_config.details, key, value); + }); + std::config_buffer::upsert(updated_config); + } + + /// This can be called by on-chain governance to update on-chain evm config + /// for the next epoch. Values are plain u128 scalars. + /// Example usage: + /// ``` + /// supra_framework::evm_config::upsert_config_for_next_epoch( + /// &framework_signer, vector["config_key"], vector[new_value]); + /// supra_framework::supra_governance::reconfigure(&framework_signer); + /// ``` + public fun upsert_config_for_next_epoch( + account: &signer, + keys: vector, + values: vector + ) acquires EvmScalarConfig { + system_addresses::assert_supra_framework(account); + assert!(!vector::is_empty(&keys), error::invalid_argument(EEMPTY_DATA)); + assert!( + vector::length(&keys) == vector::length(&values), + error::invalid_argument(EKEYS_VALUES_MISMATCH) + ); + if (!exists(@supra_framework)) { + let evm_config = + EvmScalarConfig { config: simple_map::new_from(keys, values) }; + // Config did not exist earlier, so validate the config for + // presence of required keys and value type match + validate_scalar_config(&evm_config); + std::config_buffer::upsert(evm_config); + return; + }; + // Copy existing config + let updated_config = *borrow_global(@supra_framework); + // We are never removing existing keys so by induction if all required + // keys are present in config during initialization + // they will be there later as well, so no need to validate here + vector::zip(keys, values, |key, value| { + simple_map::upsert(&mut updated_config.config, key, value); + }); + std::config_buffer::upsert(updated_config); + } + + #[view] + /// Returns the contract address stored under `key` in the EvmContractsDetails map. + /// Aborts with EKEY_NOT_FOUND if the resource has not been initialised or the key + /// is not present in the map. + public fun get_contract_value(key: String): address acquires EvmContractsDetails { + assert!(exists(@supra_framework), error::not_found(EKEY_NOT_FOUND)); + let details = borrow_global(@supra_framework); + assert!( + simple_map::contains_key(&details.details, &key), + error::not_found(EKEY_NOT_FOUND) + ); + *simple_map::borrow(&details.details, &key) + } + + #[view] + /// Returns the u128 value stored under `key` in the EvmScalarConfig map. + /// Aborts with EKEY_NOT_FOUND if the resource has not been initialised or the key + /// is not present in the map. + public fun get_scalar_config_value(key: String): u128 acquires EvmScalarConfig { + assert!(exists(@supra_framework), error::not_found(EKEY_NOT_FOUND)); + let config = borrow_global(@supra_framework); + assert!( + simple_map::contains_key(&config.config, &key), + error::not_found(EKEY_NOT_FOUND) + ); + *simple_map::borrow(&config.config, &key) + } + + fun validate_scalar_config(evm_config: &EvmScalarConfig) { + let required_keys = vector[string::utf8(CONFIG_KEY_EVM_GAS_NORMALIZATION_DENOM), + string::utf8(CONFIG_KEY_EVM_GAS_ESTIMATE_MARGIN), + string::utf8(SUPRA_EVM_GAS_USED_RATIO), + string::utf8(SUPRA_EVM_GAS_USED_RATIO_DECIMAL_PRECISION)]; + // With u128 as the value type, the Move type system prevents type mismatches at + // compile time. The only meaningful runtime check is key presence. + vector::for_each_reverse(required_keys, |rk| { + assert!(simple_map::contains_key(&evm_config.config, &rk), + error::invalid_argument(EMISSING_KEY_OR_INCORRECT_VAL_TYPE) + ); + assert!(*simple_map::borrow(&evm_config.config, &rk) > 0u128, error::invalid_argument(EMISSING_KEY_OR_INCORRECT_VAL_TYPE)); + }); + let gas_used_ratio = *simple_map::borrow(&evm_config.config, &string::utf8(SUPRA_EVM_GAS_USED_RATIO)); + let gas_used_ratio_decimal_precision = *simple_map::borrow(&evm_config.config, &string::utf8(SUPRA_EVM_GAS_USED_RATIO_DECIMAL_PRECISION)); + assert!(gas_used_ratio <= gas_used_ratio_decimal_precision, error::invalid_argument(EGAS_USED_RATIO_CANNOT_EXCEED_DECIMAL_PRECISION)); + } + + fun is_valid_evm_address(addr: &address): bool { + // BCS serialises a Move `address` as a raw fixed-size 32-byte array in + // big-endian order (most-significant byte first). An EVM address is only + // 20 bytes wide, so when a 20-byte EVM address is stored in a 32-byte Move + // address the EVM bytes occupy the last 20 positions (indices 12-31) and the + // leading 12 bytes (indices 0-11) must all be zero. + // We iterate only over that prefix and bail immediately on the first non-zero + // byte to avoid unnecessary work. + let evm_addr_bytes = bcs::to_bytes(addr); + + // Sanity check: BCS encoding of an address is always 32 bytes. If somehow + // the length differs, the address cannot be valid. + if (vector::length(&evm_addr_bytes) != MOVE_ADDRESS_BYTE_LENGTH) { + return false + }; + + // Check that all 12 high-order prefix bytes are zero. A non-zero byte here + // means the value cannot fit in 20 bytes and is therefore not a valid EVM address. + let i = 0u64; + let addr_length_diff = MOVE_ADDRESS_BYTE_LENGTH - EVM_ADDRESS_BYTE_LENGTH; + while (i < addr_length_diff) { + if (*vector::borrow(&evm_addr_bytes, i) != 0u8) { + // Non-zero prefix byte found - not a valid 20-byte EVM address. + return false + }; + i = i + 1; + }; + + true + } + + /// Only used in reconfigurations to apply the pending configs in buffer, if any. + /// If supra_framework already holds the resource, overwrite it; otherwise move it in. + public(friend) fun on_new_epoch(framework: &signer) acquires EvmScalarConfig, EvmContractsDetails { + system_addresses::assert_supra_framework(framework); + if (config_buffer::does_exist()) { + //TODO: change to extract_v2 when extract_v2 is merged and available + let new_config = config_buffer::extract(); + if (!exists(@supra_framework)) { + move_to(framework, new_config); + } else { + let old_config = borrow_global_mut(@supra_framework); + *old_config = new_config; + }; + event::emit(new_config) + }; + if (config_buffer::does_exist()) { + // change to extract_v2 when it is available + let new_config = config_buffer::extract(); + if (!exists(@supra_framework)) { + move_to(framework, new_config); + } else { + let old_config = borrow_global_mut(@supra_framework); + *old_config = new_config; + }; + event::emit(new_config) + }; + } + + // ------------------------------------------------------------------------- + // Tests + // ------------------------------------------------------------------------- + + #[test] + /// Verifies that a small address value (@0x1) is accepted as a valid EVM address. + /// + /// @0x1 is the 256-bit integer 1. BCS serialises a Move address as a raw + /// big-endian 32-byte array, so the most-significant byte is at index 0 and the + /// least-significant byte (0x01) is at index 31. The 12 leading bytes + /// (indices 0-11) are therefore all zero, and the function must return true. + fun test_valid_evm_address_small_value() { + // BCS big-endian layout for @0x1 (32 bytes): + // index 0 : 0x00 + // ... + // index 30 : 0x00 + // index 31 : 0x01 + // Leading bytes 0-11 are all 0x00 -> valid 20-byte EVM address. + assert!(is_valid_evm_address(&@0x1), 0); + } + + #[test] + /// Verifies that a full 32-byte address is rejected as an invalid EVM address. + /// + /// @0x1000...000 (64 hex digits, most-significant nibble = 1) is the 256-bit + /// value 2^252. BCS serialises it as a big-endian byte array, so the + /// most-significant byte (0x10) lands at index 0 - which falls inside the + /// leading window [0, 11] that must be entirely zero for a valid EVM address. + /// The function must therefore return false. + fun test_invalid_evm_address_full_32_bytes() { + // BCS big-endian layout for @0x1000...000 (32 bytes): + // index 0 : 0x10 <- non-zero prefix byte -> invalid EVM address + // index 1 : 0x00 + // ... + // index 31 : 0x00 + assert!( + is_valid_evm_address( + &@0x0000000000000000000000001000000000000000000000000000000000000001 + ), + 0 + ); + + assert!( + !is_valid_evm_address( + &@0x1000000000000000000000000000000000000000000000000000000000000000 + ), + 0 + ); + assert!( + !is_valid_evm_address( + &@0x0000000000000000000000011000000000000000000000000000000000000001 + ), + 0 + ); + + } + + #[test(supra_framework = @supra_framework)] + /// Happy-path test: both get_contract_value and get_scalar_config_value return the + /// values that were seeded into their respective resources. + fun test_get_contract_and_config_value_success(supra_framework: signer) acquires EvmContractsDetails, EvmScalarConfig { + // Seed EvmContractsDetails with a single entry. + let contract_key = std::string::utf8(b"usdc_contract"); + let contract_addr = @0xA550C18; + move_to(&supra_framework, EvmContractsDetails { + details: simple_map::new_from(vector[contract_key], vector[contract_addr]) + }); + + // Seed EvmScalarConfig with a single entry using a plain u128 value. + let config_key = std::string::utf8(b"evm_gas_normalization_denom"); + let config_value = 42u128; + move_to(&supra_framework, EvmScalarConfig { + config: simple_map::new_from(vector[config_key], vector[config_value]) + }); + + // Both lookups must return the exact values that were inserted above. + assert!(get_contract_value(std::string::utf8(b"usdc_contract")) == contract_addr, 0); + assert!( + get_scalar_config_value(std::string::utf8(b"evm_gas_normalization_denom")) == 42u128, + 0 + ); + } + + #[test(supra_framework = @supra_framework)] + /// Failure test: get_contract_value aborts when the requested key is absent. + /// + /// The expected abort code is error::not_found(EKEY_NOT_FOUND) + /// = (NOT_FOUND_CATEGORY=6 << 16) | EKEY_NOT_FOUND=4 = 0x60004. + #[expected_failure(abort_code = 0x60004, location = supra_framework::evm_config)] + fun test_get_contract_value_key_not_found(supra_framework: signer) acquires EvmContractsDetails { + move_to(&supra_framework, EvmContractsDetails { + details: simple_map::new_from( + vector[std::string::utf8(b"existing_key")], + vector[@0x1] + ) + }); + get_contract_value(std::string::utf8(b"nonexistent_key")); + } + + #[test(supra_framework = @supra_framework)] + /// Failure test: get_scalar_config_value aborts when the requested key is absent. + /// + /// The expected abort code is error::not_found(EKEY_NOT_FOUND) + /// = (NOT_FOUND_CATEGORY=6 << 16) | EKEY_NOT_FOUND=4 = 0x60004. + #[expected_failure(abort_code = 0x60004, location = supra_framework::evm_config)] + fun test_get_config_value_key_not_found(supra_framework: signer) acquires EvmScalarConfig { + move_to(&supra_framework, EvmScalarConfig { + config: simple_map::new_from( + vector[std::string::utf8(b"existing_key")], + vector[1u128] + ) + }); + get_scalar_config_value(std::string::utf8(b"nonexistent_key")); + } + + #[test] + /// Success test: validate_scalar_config must not abort when all required keys are present. + /// + /// evm_gas_normalization_denom is the sole required key. A config seeded with that + /// key and a plain u128 value must pass validation. + fun test_validate_config_success() { + let config_key = std::string::utf8(CONFIG_KEY_EVM_GAS_NORMALIZATION_DENOM); + let estimate_margin_key = std::string::utf8(CONFIG_KEY_EVM_GAS_ESTIMATE_MARGIN); + let gas_used_ratio_key = std::string::utf8(SUPRA_EVM_GAS_USED_RATIO); + let decimal_precision_key = std::string::utf8(SUPRA_EVM_GAS_USED_RATIO_DECIMAL_PRECISION); + + + let evm_config = EvmScalarConfig { + config: simple_map::new_from(vector[config_key, estimate_margin_key, gas_used_ratio_key, decimal_precision_key], vector[100u128, 1000u128, 500u128, 1000u128]) + }; + // Must complete without aborting. + validate_scalar_config(&evm_config); + } + + #[test] + /// Failure test: validate_scalar_config aborts when a required key is absent. + /// + /// An empty config is missing evm_gas_normalization_denom, so validation must abort. + /// The expected abort code is error::invalid_argument(EMISSING_KEY_OR_INCORRECT_VAL_TYPE) + /// = (INVALID_ARGUMENT_CATEGORY=1 << 16) | EMISSING_KEY_OR_INCORRECT_VAL_TYPE=5 = 0x10005. + #[expected_failure(abort_code = 0x10005, location = supra_framework::evm_config)] + fun test_validate_config_missing_required_key() { + // An empty config is missing the sole required key -> must abort. + let evm_config = EvmScalarConfig { config: simple_map::new() }; + validate_scalar_config(&evm_config); + } + + #[test(supra_framework = @supra_framework)] + /// Failure test: upsert_evm_contract_details_for_next_epoch aborts when any + /// address in the values vector is not a valid EVM address. + /// + /// The call passes one valid address (@0x1, which fits in 20 bytes with all + /// leading bytes zero) alongside one invalid address whose most-significant + /// byte is non-zero (@0x1000...000). The presence of the invalid address + /// must trigger EINVALID_EVM_ADDRESS regardless of position in the vector. + /// The expected abort code is error::invalid_argument(EINVALID_EVM_ADDRESS) + /// = (INVALID_ARGUMENT_CATEGORY=1 << 16) | EINVALID_EVM_ADDRESS=3 = 0x10003. + #[expected_failure(abort_code = 0x10003, location = supra_framework::evm_config)] + fun test_upsert_evm_contract_details_mixed_invalid_address(supra_framework: signer) acquires EvmContractsDetails { + let keys = vector[ + std::string::utf8(b"valid_contract"), + std::string::utf8(b"invalid_contract") + ]; + let values = vector[ + @0x1, // valid: fits in 20 bytes + @0x1000000000000000000000000000000000000000000000000000000000000000 // invalid: high byte non-zero + ]; + upsert_evm_contract_details_for_next_epoch(&supra_framework, keys, values); + } + + #[test(supra_framework = @supra_framework)] + /// Failure test: validate_scalar_config aborts when gas used ratio exceeds decimal precision. + /// + /// The config includes all required keys, but the value for SUPRA_EVM_GAS_USED_RATIO (500) exceeds the value for SUPRA_EVM_GAS_USED_RATIO_DECIMAL_PRECISION (1000), which is not logically valid. Validation must abort with + /// error::invalid_argument(EGAS_USED_RATIO_CANNOT_EXCEED_DECIMAL_PRECISION) + /// = (INVALID_ARGUMENT_CATEGORY=1 << 16) | EGAS_USED_RATIO_CANNOT_EXCEED_DECIMAL_PRECISION=7 = 0x10007. + #[expected_failure(abort_code = 0x10007, location = supra_framework::evm_config)] + fun test_validate_config_gas_used_ratio_exceeds_decimal_precision() { + let config_key = std::string::utf8(CONFIG_KEY_EVM_GAS_NORMALIZATION_DENOM); + let estimate_margin_key = std::string::utf8(CONFIG_KEY_EVM_GAS_ESTIMATE_MARGIN); + let gas_used_ratio_key = std::string::utf8(SUPRA_EVM_GAS_USED_RATIO); + let decimal_precision_key = std::string::utf8(SUPRA_EVM_GAS_USED_RATIO_DECIMAL_PRECISION); + let evm_config = EvmScalarConfig { + config: simple_map::new_from( + vector[config_key, estimate_margin_key, gas_used_ratio_key, decimal_precision_key], + vector[100u128, 1000u128, 1500u128, 1000u128] // gas used ratio exceeds decimal precision -> must abort + ) + }; + validate_scalar_config(&evm_config); + +} + +} diff --git a/aptos-move/framework/supra-framework/sources/genesis.move b/aptos-move/framework/supra-framework/sources/genesis.move index b202ff61e54..e123fadd181 100644 --- a/aptos-move/framework/supra-framework/sources/genesis.move +++ b/aptos-move/framework/supra-framework/sources/genesis.move @@ -5,6 +5,7 @@ module supra_framework::genesis { use std::string::String; use std::vector; use aptos_std::simple_map; + use supra_framework::evm_config; use supra_framework::account; use supra_framework::aggregator_factory; @@ -300,6 +301,17 @@ module supra_framework::genesis { evm_genesis_config::initialize(supra_framework, evm_genesis_config); } + /// Initialize the EVM config. + fun initialize_evm_config( + supra_framework: &signer, + contract_names: vector, + contract_addresses: vector
, + config_keys: vector, + config_values: vector + ) { + evm_config::initialize(supra_framework, contract_names, contract_addresses, config_keys, config_values); + } + /// Initialize the leader ban config fun initialize_leader_ban_registry_config( supra_framework: &signer, leader_ban_registry_config: vector diff --git a/aptos-move/framework/supra-framework/sources/reconfiguration_with_dkg.move b/aptos-move/framework/supra-framework/sources/reconfiguration_with_dkg.move index c272aaa336c..598aecdfaf1 100644 --- a/aptos-move/framework/supra-framework/sources/reconfiguration_with_dkg.move +++ b/aptos-move/framework/supra-framework/sources/reconfiguration_with_dkg.move @@ -7,6 +7,7 @@ module supra_framework::reconfiguration_with_dkg { use std::features; use std::option; use std::vector; + use supra_framework::evm_config; use supra_framework::automation_registry; use supra_framework::randomness; use supra_framework::consensus_config; @@ -107,6 +108,7 @@ module supra_framework::reconfiguration_with_dkg { randomness_api_v0_config::on_new_epoch(framework); evm_genesis_config::on_new_epoch(framework); dkg_config::on_new_epoch(framework); + evm_config::on_new_epoch(framework); reconfiguration::reconfigure(); } diff --git a/aptos-move/vm-genesis/src/lib.rs b/aptos-move/vm-genesis/src/lib.rs index d4132f464ad..0a45efde5e6 100644 --- a/aptos-move/vm-genesis/src/lib.rs +++ b/aptos-move/vm-genesis/src/lib.rs @@ -56,6 +56,7 @@ use std::{ collections::BTreeSet, hash::{Hash, Hasher}, }; +use aptos_types::on_chain_config::{OnChainEvmContractsDetails, OnChainEvmConfig}; // The seed is arbitrarily picked to produce a consistent key. XXX make this more formal? const GENESIS_SEED: [u8; 32] = [42; 32]; @@ -248,6 +249,8 @@ pub fn encode_genesis_transaction_for_testnet( gas_schedule: &GasScheduleV2, supra_config_bytes: Vec, evm_genesis_config: Option, + evm_contracts_details: Option, + evm_scalar_config: Option ) -> Transaction { Transaction::GenesisTransaction(WriteSetPayload::Direct( encode_genesis_change_set_for_testnet( @@ -268,6 +271,8 @@ pub fn encode_genesis_transaction_for_testnet( gas_schedule, supra_config_bytes, evm_genesis_config, + evm_contracts_details, + evm_scalar_config ), )) } @@ -290,6 +295,9 @@ pub fn encode_genesis_change_set_for_testnet( gas_schedule: &GasScheduleV2, supra_config_bytes: Vec, evm_genesis_config: Option, + evm_contracts_details: Option, + evm_scalar_config: Option + ) -> ChangeSet { validate_genesis_config(genesis_config); // Create a Move VM session so we can invoke on-chain genesis initializations. @@ -343,6 +351,10 @@ pub fn encode_genesis_change_set_for_testnet( initialize_evm_genesis_config(&mut session, &evm_genesis_config); } + if let (Some(evm_contracts_details), Some(evm_scalar_config)) = (evm_contracts_details, evm_scalar_config) { + initialize_evm_config(&mut session, evm_contracts_details, evm_scalar_config); + } + create_accounts(&mut session, accounts); if let Some(owner_group) = owner_group { @@ -619,6 +631,30 @@ fn initialize_evm_genesis_config( ); } +fn initialize_evm_config( + session: &mut SessionExt, + evm_contracts_details: OnChainEvmContractsDetails, + evm_scalar_config: OnChainEvmConfig, +) { + + let (contract_names, contract_addresses) = evm_contracts_details.to_move_values(); + let (config_keys, config_values) = evm_scalar_config.to_move_values(); + + exec_function( + session, + GENESIS_MODULE_NAME, + "initialize_evm_config", + vec![], + serialize_values(&vec![ + MoveValue::Signer(CORE_CODE_ADDRESS), + contract_names, + contract_addresses, + config_keys, + config_values + ]), + ); +} + fn initialize_config_buffer(session: &mut SessionExt) { exec_function( session, @@ -1287,6 +1323,8 @@ pub fn generate_test_genesis( &default_gas_schedule(), b"test".to_vec(), None, + None, + None, ); (genesis, test_validators) } @@ -1318,6 +1356,8 @@ pub fn generate_mainnet_genesis( &default_gas_schedule(), b"test".to_vec(), None, + None, + None, ); (genesis, test_validators) } diff --git a/crates/aptos-genesis/src/lib.rs b/crates/aptos-genesis/src/lib.rs index d53bf924acc..aca84febb4c 100644 --- a/crates/aptos-genesis/src/lib.rs +++ b/crates/aptos-genesis/src/lib.rs @@ -177,6 +177,8 @@ impl GenesisInfo { &self.gas_schedule, b"test".to_vec(), None, + None, + None, ) } diff --git a/devtools/assets/license_header.txt b/devtools/assets/license_header.txt index ebc846ed74b..47bc4ada806 100644 --- a/devtools/assets/license_header.txt +++ b/devtools/assets/license_header.txt @@ -1,2 +1,2 @@ -Copyright (c) Aptos Foundation +Copyright (c) Supra. SPDX-License-Identifier: Apache-2.0 diff --git a/devtools/assets/license_header_utf8.txt b/devtools/assets/license_header_utf8.txt index 5f8d7b55e16..c0cb30e1214 100644 --- a/devtools/assets/license_header_utf8.txt +++ b/devtools/assets/license_header_utf8.txt @@ -1,2 +1,2 @@ -Copyright © Aptos Foundation +Copyright © Supra. SPDX-License-Identifier: Apache-2.0 diff --git a/devtools/assets/shared_license_header.txt b/devtools/assets/shared_license_header.txt index 18a8743e5f4..a279050a3ae 100644 --- a/devtools/assets/shared_license_header.txt +++ b/devtools/assets/shared_license_header.txt @@ -1,3 +1,4 @@ +Copyright © Supra. Copyright (c) Aptos Foundation Parts of the project are originally copyright (c) Meta Platforms, Inc. SPDX-License-Identifier: Apache-2.0 diff --git a/devtools/assets/shared_license_header_utf8.txt b/devtools/assets/shared_license_header_utf8.txt index e5b2ef62fc0..78c2c2b1913 100644 --- a/devtools/assets/shared_license_header_utf8.txt +++ b/devtools/assets/shared_license_header_utf8.txt @@ -1,3 +1,4 @@ +Copyright © Supra. Copyright © Aptos Foundation Parts of the project are originally copyright © Meta Platforms, Inc. SPDX-License-Identifier: Apache-2.0 diff --git a/types/src/on_chain_config/automation_registry.rs b/types/src/on_chain_config/automation_registry.rs index 4f68aa12669..e910f61baad 100644 --- a/types/src/on_chain_config/automation_registry.rs +++ b/types/src/on_chain_config/automation_registry.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2025 Supra. // SPDX-License-Identifier: Apache-2.0 diff --git a/types/src/on_chain_config/evm_config.rs b/types/src/on_chain_config/evm_config.rs new file mode 100644 index 00000000000..e3644621ba1 --- /dev/null +++ b/types/src/on_chain_config/evm_config.rs @@ -0,0 +1,553 @@ +// Copyright (c) 2026 Supra. +// SPDX-License-Identifier: Apache-2.0 + +use crate::on_chain_config::OnChainConfig; +use move_core_types::account_address::AccountAddress; +use move_core_types::value::MoveValue; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +// -- EvmContractName ----------------------------------------------------------- + +/// Evm Contract Names deployed by Supra at genesis or later to be part of Supra EVM main state. +/// Currently only system targeted contracts have dedicated enum variants, the rest will be stored +/// as instance of [Self::Custom] variant. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Hash, PartialOrd, Ord)] +pub enum EvmContractName { + BlockMetadata, + AutomationRegistry, + Custom(String), +} + +impl FromStr for EvmContractName { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "BlockMetadata" => Ok(EvmContractName::BlockMetadata), + "AutomationRegistry" => Ok(EvmContractName::AutomationRegistry), + n => Ok(EvmContractName::Custom(n.to_string())), + } + } +} + +impl Display for EvmContractName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EvmContractName::BlockMetadata => write!(f, "BlockMetadata"), + EvmContractName::AutomationRegistry => write!(f, "AutomationRegistry"), + EvmContractName::Custom(n) => write!(f, "{n}"), + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub(crate) struct EvmContractsDetails { + details: Vec<(String, AccountAddress)>, +} + +pub const EVM_ADDRESS_LENGTH: usize = 20; +type RawEvmAddress = [u8; EVM_ADDRESS_LENGTH]; + +/// The Genesis configuration for EVM that can only be set once at genesis epoch. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct OnChainEvmContractsDetails { + pub name_to_addresses: BTreeMap, +} + +impl OnChainEvmContractsDetails { + /// Converts keys and values to [`MoveValue`] and returns as tuple. + pub fn to_move_values(self) -> (MoveValue, MoveValue) { + let (keys, values): (Vec<_>, Vec<_>) = self + .name_to_addresses + .into_iter() + .map(|(key, value)| { + // A Move `address` is 32 bytes serialised by BCS in big-endian order + // (most-significant byte at index 0). An EVM address is only 20 bytes, + // so we zero-pad the leading 12 bytes and place the EVM bytes at the + // tail (indices 12-31). This matches the invariant enforced by + // `is_valid_evm_address` in evm_config.move, which asserts that + // indices 0-11 are all zero. + let mut padded = [0u8; AccountAddress::LENGTH]; + padded[AccountAddress::LENGTH - EVM_ADDRESS_LENGTH..].copy_from_slice(&value); + ( + MoveValue::vector_u8(key.to_string().into_bytes()), + AccountAddress::from(padded), + ) + }) + .unzip(); + (MoveValue::Vector(keys), MoveValue::vector_address(values)) + } + + pub fn get(&self, contract_name: EvmContractName) -> Option<&RawEvmAddress> { + self.name_to_addresses.get(&contract_name) + } + + pub fn has_all_keys(&self, keys: &[EvmContractName]) -> bool { + keys.into_iter() + .all(|key| self.name_to_addresses.contains_key(key)) + } +} + +impl OnChainConfig for OnChainEvmContractsDetails { + const MODULE_IDENTIFIER: &'static str = "evm_config"; + const TYPE_IDENTIFIER: &'static str = "EvmContractsDetails"; + + fn deserialize_into_config(bytes: &[u8]) -> anyhow::Result { + let raw_details = bcs::from_bytes::(bytes)?; + let details = raw_details + .details + .into_iter() + .filter_map(|(key, value)| { + let name = EvmContractName::from_str(&key).ok()?; + // BCS serialises a Move `address` as a raw 32-byte big-endian + // array. `to_move_values` places the 20-byte EVM address in the + // tail (indices 12-31) with the leading 12 bytes zeroed, so we + // must read from the back to recover the original EVM address. + let evm_address: RawEvmAddress = value.into_bytes() + [AccountAddress::LENGTH - EVM_ADDRESS_LENGTH..] + .try_into() + .ok()?; + Some((name, evm_address)) + }) + .collect::>(); + Ok(Self { + name_to_addresses: details, + }) + } +} + +// -- EvmScalarConfigKey -------------------------------------------------------- + +/// Typed keys for entries stored in the on-chain `EvmScalarConfig` resource. +/// Only keys whose string representation is known at compile time are listed +/// here; unknown keys are silently skipped during deserialization so that an +/// older node binary can still read a config that was extended with new keys. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Hash, PartialOrd, Ord)] +pub enum EvmScalarConfigKey { + EvmGasNormalizationDenom, + EvmGasEstimateMargin, + EvmGasUsedRatio, + EvmGasUsedRatioDecimalPrecision, +} + +impl EvmScalarConfigKey { + /// The set of keys that MUST be present in the on-chain config. + /// Deserialization hard-fails if any of these is missing. + pub fn required_keys() -> &'static [EvmScalarConfigKey] { + &[ + EvmScalarConfigKey::EvmGasNormalizationDenom, + EvmScalarConfigKey::EvmGasEstimateMargin, + EvmScalarConfigKey::EvmGasUsedRatio, + EvmScalarConfigKey::EvmGasUsedRatioDecimalPrecision, + ] + } +} + +impl FromStr for EvmScalarConfigKey { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "evm_gas_normalization_denom" => Ok(EvmScalarConfigKey::EvmGasNormalizationDenom), + "evm_gas_estimate_margin" => Ok(EvmScalarConfigKey::EvmGasEstimateMargin), + "evm_gas_used_ratio" => Ok(EvmScalarConfigKey::EvmGasUsedRatio), + "evm_gas_used_ratio_decimal_precision" => { + Ok(EvmScalarConfigKey::EvmGasUsedRatioDecimalPrecision) + }, + _ => Err(anyhow::anyhow!("unknown evm scalar config key: {}", s)), + } + } +} + +impl Display for EvmScalarConfigKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + // Mirror FromStr: Display produces the canonical snake_case string that + // operators see in logs and error messages. + match self { + EvmScalarConfigKey::EvmGasNormalizationDenom => { + write!(f, "evm_gas_normalization_denom") + }, + EvmScalarConfigKey::EvmGasEstimateMargin => { + write!(f, "evm_gas_estimate_margin") + }, + EvmScalarConfigKey::EvmGasUsedRatio => { + write!(f, "evm_gas_used_ratio") + }, + EvmScalarConfigKey::EvmGasUsedRatioDecimalPrecision => { + write!(f, "evm_gas_used_ratio_decimal_precision") + }, + } + } +} + +// -- Internal BCS mirror struct ------------------------------------------------ + +/// BCS mirror of the on-chain `EvmScalarConfig` resource. +/// `SimpleMap` serializes as `Vec<(K, V)>` in BCS. +/// All config values are plain u128 scalars; the Move type system enforces +/// type correctness at compile time. +#[derive(Deserialize, Serialize)] +struct RawEvmScalarConfig { + config: Vec<(String, u128)>, +} + +// -- OnChainEvmConfig ---------------------------------------------------------- + +/// The decoded, type-safe representation of the on-chain `EvmScalarConfig` resource. +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct OnChainEvmConfig { + pub config: BTreeMap, +} + +impl OnChainEvmConfig { + /// Convenience accessor for the EVM gas normalisation denominator. + pub fn evm_gas_normalization_denom(&self) -> anyhow::Result { + self.config + .get(&EvmScalarConfigKey::EvmGasNormalizationDenom) + .copied() + .ok_or_else(|| { + anyhow::anyhow!("evm_gas_normalization_denom not found in EvmScalarConfig") + }) + } + + pub fn evm_gas_estimate_margin(&self) -> anyhow::Result { + self.config + .get(&EvmScalarConfigKey::EvmGasEstimateMargin) + .copied() + .and_then(|v| v.try_into().ok()) + .ok_or_else(|| anyhow::anyhow!("evm_gas_estimate_margin not found in EvmScalarConfig")) + } + + pub fn evm_gas_used_ratio(&self) -> anyhow::Result { + // Decimal is guaranteed to be non-zero due to validation in evm_config.move, so this division is safe. + let decimal = self.evm_gas_used_ratio_decimal_precision()?; + self.config + .get(&EvmScalarConfigKey::EvmGasUsedRatio) + .copied() + .and_then(|v| { + let ratio = v as f64 / decimal as f64; + if ratio > 0.0 { + Some(ratio) + } else { + None + } + }) + .ok_or_else(|| anyhow::anyhow!("evm_gas_used_ratio not found in EvmScalarConfig")) + } + + pub fn evm_gas_used_ratio_decimal_precision(&self) -> anyhow::Result { + self.config + .get(&EvmScalarConfigKey::EvmGasUsedRatioDecimalPrecision) + .copied() + .and_then(|v| v.try_into().ok()) + .ok_or_else(|| { + anyhow::anyhow!("evm_gas_used_ratio_decimal_precision not found in EvmScalarConfig") + }) + } + + pub fn to_move_values(self) -> (MoveValue, MoveValue) { + let (keys, values): (Vec<_>, Vec<_>) = self + .config + .into_iter() + .map(|(key, value)| { + ( + MoveValue::vector_u8(key.to_string().into_bytes()), + MoveValue::U128(value), + ) + }) + .unzip(); + (MoveValue::Vector(keys), MoveValue::Vector(values)) + } +} + +impl OnChainConfig for OnChainEvmConfig { + const MODULE_IDENTIFIER: &'static str = "evm_config"; + const TYPE_IDENTIFIER: &'static str = "EvmScalarConfig"; + + fn deserialize_into_config(bytes: &[u8]) -> anyhow::Result { + let raw: RawEvmScalarConfig = bcs::from_bytes(bytes) + .map_err(|e| anyhow::anyhow!("failed to BCS-decode RawEvmScalarConfig: {}", e))?; + + let mut config = BTreeMap::new(); + for (key_str, value) in raw.config { + // Skip keys that are not (yet) known to this codebase; this allows + // the on-chain config to grow new entries without breaking older nodes. + let key = match EvmScalarConfigKey::from_str(&key_str) { + Ok(k) => k, + Err(_) => continue, + }; + config.insert(key, value); + } + + // Hard-fail if any required key is absent - the node cannot operate + // correctly without the full set of mandatory config values. + for required_key in EvmScalarConfigKey::required_keys() { + if !config.contains_key(required_key) { + return Err(anyhow::anyhow!( + "required EvmScalarConfig key '{}' is missing from on-chain state", + required_key // Display -> "evm_gas_normalization_denom" + )); + } + } + + Ok(Self { config }) + } +} + +#[cfg(test)] +mod unit_tests { + use super::*; + use crate::on_chain_config::{InMemoryOnChainConfig, OnChainConfig, OnChainConfigProvider}; + use std::collections::HashMap; + + // -- Helpers --------------------------------------------------------------- + + /// Returns a `RawEvmScalarConfig` with all four required keys populated. + /// Use this as the baseline for any test that needs a fully valid config. + fn raw_evm_scalar_config_all_keys( + denom: u128, + margin: u128, + gas_used_ratio: u128, + decimal_precision: u128, + ) -> RawEvmScalarConfig { + RawEvmScalarConfig { + config: vec![ + ("evm_gas_normalization_denom".to_string(), denom), + ("evm_gas_estimate_margin".to_string(), margin), + ("evm_gas_used_ratio".to_string(), gas_used_ratio), + ( + "evm_gas_used_ratio_decimal_precision".to_string(), + decimal_precision, + ), + ], + } + } + + /// Convenience wrapper with fixed margin/ratio/precision for tests that + /// only care about the denom value. + fn raw_evm_scalar_config_with_denom(denom: u128) -> RawEvmScalarConfig { + raw_evm_scalar_config_all_keys(denom, 500, 750, 1000) + } + + // -- OnChainEvmConfig deserialization -------------------------------------- + + #[test] + fn test_deserialize_evm_config_happy_path() { + let bytes = bcs::to_bytes(&raw_evm_scalar_config_with_denom(100)).unwrap(); + let config = OnChainEvmConfig::deserialize_into_config(&bytes).unwrap(); + assert_eq!(config.evm_gas_normalization_denom().unwrap(), 100u128); + // All four required keys must be decoded - no phantom entries. + assert_eq!(config.config.len(), 4); + } + + #[test] + fn test_deserialize_evm_config_missing_required_key() { + // Empty config - all required keys absent; the first one iterated + // (evm_gas_normalization_denom) must appear in the error. + let raw = RawEvmScalarConfig { config: vec![] }; + let bytes = bcs::to_bytes(&raw).unwrap(); + let result = OnChainEvmConfig::deserialize_into_config(&bytes); + assert!(result.is_err(), "expected Err when required key is missing"); + assert!( + result + .unwrap_err() + .to_string() + .contains("evm_gas_normalization_denom"), + "error should identify the missing key" + ); + } + + #[test] + fn test_deserialize_evm_config_each_required_key_missing() { + // Every required key must be individually enforced: removing any single + // one must hard-fail with an error message that names that key. + let required = [ + "evm_gas_normalization_denom", + "evm_gas_estimate_margin", + "evm_gas_used_ratio", + "evm_gas_used_ratio_decimal_precision", + ]; + let full = raw_evm_scalar_config_all_keys(100, 500, 750, 1000); + for missing_key in &required { + let partial = RawEvmScalarConfig { + config: full + .config + .iter() + .filter(|(k, _)| k != missing_key) + .cloned() + .collect(), + }; + let bytes = bcs::to_bytes(&partial).unwrap(); + let result = OnChainEvmConfig::deserialize_into_config(&bytes); + assert!( + result.is_err(), + "expected Err when '{}' is absent", + missing_key + ); + assert!( + result.unwrap_err().to_string().contains(missing_key), + "error must name the missing key '{}'", + missing_key + ); + } + } + + #[test] + fn test_deserialize_evm_config_unknown_key_skipped() { + // Unknown keys must be silently dropped so an older node binary can still + // read a config that was extended with new keys by a newer software version. + let mut raw = raw_evm_scalar_config_all_keys(42, 500, 750, 1000); + raw.config + .push(("completely_unknown_key".to_string(), 7u128)); + let bytes = bcs::to_bytes(&raw).unwrap(); + let config = OnChainEvmConfig::deserialize_into_config(&bytes).unwrap(); + assert_eq!(config.evm_gas_normalization_denom().unwrap(), 42u128); + // Only the four known keys should appear; the unknown one is dropped. + assert_eq!(config.config.len(), 4); + } + + // -- Accessor correctness -------------------------------------------------- + + #[test] + fn test_accessor_evm_gas_estimate_margin() { + let bytes = bcs::to_bytes(&raw_evm_scalar_config_all_keys(100, 1234, 750, 1000)).unwrap(); + let config = OnChainEvmConfig::deserialize_into_config(&bytes).unwrap(); + assert_eq!(config.evm_gas_estimate_margin().unwrap(), 1234u64); + } + + #[test] + fn test_accessor_evm_gas_used_ratio_decimal_precision() { + let bytes = bcs::to_bytes(&raw_evm_scalar_config_all_keys(100, 500, 750, 2000)).unwrap(); + let config = OnChainEvmConfig::deserialize_into_config(&bytes).unwrap(); + assert_eq!( + config.evm_gas_used_ratio_decimal_precision().unwrap(), + 2000u64 + ); + } + + #[test] + fn test_accessor_evm_gas_used_ratio() { + // ratio=750, decimal_precision=1000 -> 750 / 1000 = 0.75 + let bytes = bcs::to_bytes(&raw_evm_scalar_config_all_keys(100, 500, 750, 1000)).unwrap(); + let config = OnChainEvmConfig::deserialize_into_config(&bytes).unwrap(); + let ratio = config.evm_gas_used_ratio().unwrap(); + assert!( + (ratio - 0.75f64).abs() < f64::EPSILON, + "expected 0.75, got {}", + ratio + ); + } + + // -- to_move_values -------------------------------------------------------- + + #[test] + fn test_to_move_values_structure() { + // to_move_values must produce two parallel MoveValue::Vector outputs: + // keys as vector (UTF-8 strings) and values as u128. + let config = OnChainEvmConfig { + config: [ + (EvmScalarConfigKey::EvmGasNormalizationDenom, 42u128), + (EvmScalarConfigKey::EvmGasEstimateMargin, 500u128), + (EvmScalarConfigKey::EvmGasUsedRatio, 750u128), + ( + EvmScalarConfigKey::EvmGasUsedRatioDecimalPrecision, + 1000u128, + ), + ] + .into(), + }; + let (keys_mv, values_mv) = config.to_move_values(); + let MoveValue::Vector(keys) = keys_mv else { + panic!("expected MoveValue::Vector for keys") + }; + let MoveValue::Vector(values) = values_mv else { + panic!("expected MoveValue::Vector for values") + }; + assert_eq!(keys.len(), 4); + assert_eq!(values.len(), 4); + // Keys are encoded as vector (UTF-8 bytes). + assert!(matches!(keys[0], MoveValue::Vector(_))); + // Values are u128. + assert!(matches!(values[0], MoveValue::U128(_))); + } + + // -- OnChainEvmContractsDetails round-trip --------------------------------- + + #[test] + fn test_evm_contracts_details_round_trip() { + // 20-byte EVM address placed in the lower 20 bytes of a 32-byte Move + // address (upper 12 zeroed), matching the to_move_values encoding. + let evm_bytes: RawEvmAddress = [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, + 0xee, 0xff, 0x00, 0x11, 0x22, 0x33, + ]; + let original = OnChainEvmContractsDetails { + name_to_addresses: [(EvmContractName::BlockMetadata, evm_bytes)].into(), + }; + let mut padded = [0u8; AccountAddress::LENGTH]; + padded[AccountAddress::LENGTH - EVM_ADDRESS_LENGTH..].copy_from_slice(&evm_bytes); + let raw = EvmContractsDetails { + details: vec![("BlockMetadata".to_string(), AccountAddress::from(padded))], + }; + let bytes = bcs::to_bytes(&raw).unwrap(); + let recovered = OnChainEvmContractsDetails::deserialize_into_config(&bytes).unwrap(); + assert_eq!(recovered, original); + + let (_keys, addresses) = original.to_move_values(); + match addresses { + MoveValue::Vector(addrs) => { + assert_eq!(addrs.len(), 1); + match &addrs[0] { + MoveValue::Address(addr) => { + let recovered_evm_bytes: RawEvmAddress = addr.into_bytes()[AccountAddress::LENGTH - EVM_ADDRESS_LENGTH..] + .try_into() + .expect("slice with correct length"); + assert_eq!(recovered_evm_bytes, evm_bytes); + }, + _ => panic!("expected MoveValue::Address in values vector"), + } + } + _ => panic!("expected MoveValue::Vector"), + } + } + + // -- Tier 2: InMemoryOnChainConfig provider -------------------------------- + + #[test] + fn test_fetch_evm_config_via_provider() { + // Validates the full path: raw BCS bytes -> ConfigID lookup -> + // deserialize_into_config -> typed accessor, without a running VM. + let bytes = bcs::to_bytes(&raw_evm_scalar_config_with_denom(999)).unwrap(); + let mut map = HashMap::new(); + map.insert(OnChainEvmConfig::CONFIG_ID, bytes); + let provider = InMemoryOnChainConfig::new(map); + let config = provider.get::().unwrap(); + assert_eq!(config.evm_gas_normalization_denom().unwrap(), 999u128); + } + + #[test] + fn test_fetch_evm_contracts_details_via_provider() { + // Same end-to-end path for OnChainEvmContractsDetails. + let evm_bytes: RawEvmAddress = [ + 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + ]; + let mut padded = [0u8; AccountAddress::LENGTH]; + padded[AccountAddress::LENGTH - EVM_ADDRESS_LENGTH..].copy_from_slice(&evm_bytes); + let raw = EvmContractsDetails { + details: vec![("BlockMetadata".to_string(), AccountAddress::from(padded))], + }; + let bytes = bcs::to_bytes(&raw).unwrap(); + let mut map = HashMap::new(); + map.insert(OnChainEvmContractsDetails::CONFIG_ID, bytes); + let provider = InMemoryOnChainConfig::new(map); + let recovered = provider.get::().unwrap(); + assert_eq!( + recovered.get(EvmContractName::BlockMetadata), + Some(&evm_bytes) + ); + } +} diff --git a/types/src/on_chain_config/evm_genesis_config.rs b/types/src/on_chain_config/evm_genesis_config.rs index 1901bc9bb64..ca66bdc826d 100644 --- a/types/src/on_chain_config/evm_genesis_config.rs +++ b/types/src/on_chain_config/evm_genesis_config.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) Supra Foundation // SPDX-License-Identifier: Apache-2.0 @@ -32,6 +29,15 @@ pub struct GenesisEvmEOA { pub amount: u128, } +/// The Creator address and nonce determines the contract' deployment address. +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub enum ContractKind { + /// Default contract creation API + Create, + /// EVM address that creates a contract. + Call(String), +} + /// The Creator address and nonce determines the contract' deployment address. #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct GenesisEvmContract { @@ -43,6 +49,10 @@ pub struct GenesisEvmContract { pub amount: u128, /// The bytecode of the contract to deploy. pub bytecode: Vec, + /// Type of the contract + pub kind: ContractKind, + /// Precalculated address of the contract + pub deploy_address: String, } impl OnChainEvmGenesisConfig { diff --git a/types/src/on_chain_config/mod.rs b/types/src/on_chain_config/mod.rs index 50a10bd80c6..e3ade280f2c 100644 --- a/types/src/on_chain_config/mod.rs +++ b/types/src/on_chain_config/mod.rs @@ -27,6 +27,7 @@ mod automation_registry; mod chain_id; mod commit_history; mod consensus_config; +mod evm_config; mod evm_genesis_config; mod execution_config; mod gas_schedule; @@ -38,7 +39,6 @@ mod timed_features; mod timestamp; mod transaction_fee; mod validator_set; - pub use self::{ approved_execution_hashes::ApprovedExecutionHashes, aptos_features::*, @@ -56,8 +56,12 @@ pub use self::{ LeaderReputationType, OnChainConsensusConfig, ProposerAndVoterConfig, ProposerElectionType, ValidatorTxnConfig, }, + evm_config::{ + EvmContractName, EvmScalarConfigKey, OnChainEvmConfig, OnChainEvmContractsDetails, + }, evm_genesis_config::{ - GenesisEvmContract, GenesisEvmEOA, OnChainEvmGenesisConfig, EVM_GENESIS_EVENT_MOVE_TYPE_TAG, + ContractKind, GenesisEvmContract, GenesisEvmEOA, OnChainEvmGenesisConfig, + EVM_GENESIS_EVENT_MOVE_TYPE_TAG, }, execution_config::{ BlockGasLimitType, ExecutionConfigV1, ExecutionConfigV2, ExecutionConfigV4, diff --git a/types/src/transaction/automated_transaction.rs b/types/src/transaction/automated_transaction.rs index 317547326be..a7b1009ebcc 100644 --- a/types/src/transaction/automated_transaction.rs +++ b/types/src/transaction/automated_transaction.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. // SPDX-License-Identifier: Apache-2.0 diff --git a/types/src/transaction/automation.rs b/types/src/transaction/automation.rs index c374987485d..892afaa63d5 100644 --- a/types/src/transaction/automation.rs +++ b/types/src/transaction/automation.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2025 Supra. // SPDX-License-Identifier: Apache-2.0 diff --git a/types/src/unit_tests/automation.rs b/types/src/unit_tests/automation.rs index eb361d3845c..470b806479d 100644 --- a/types/src/unit_tests/automation.rs +++ b/types/src/unit_tests/automation.rs @@ -1,6 +1,3 @@ -// Copyright (c) Aptos Foundation -// SPDX-License-Identifier: Apache-2.0 - // Copyright (c) 2024 Supra. // SPDX-License-Identifier: Apache-2.0