diff --git a/Cargo.lock b/Cargo.lock index ba4c885477ce9..48406d844fb53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3324,18 +3324,26 @@ dependencies = [ "aptos-genesis", "aptos-infallible", "aptos-keygen", + "aptos-language-e2e-tests", + "aptos-move-debugger", "aptos-rest-client", "aptos-temppath", "aptos-types", + "aptos-vm", + "aptos-vm-logging", "bcs 0.1.4", "clap 4.4.14", "futures", "git2 0.16.1", "handlebars", "hex", + "move-binary-format", + "move-bytecode-verifier", "move-core-types", "move-model", + "move-vm-types", "once_cell", + "parking_lot 0.12.1", "reqwest", "serde", "serde_json", @@ -3345,6 +3353,7 @@ dependencies = [ "strum_macros 0.24.3", "tokio", "url", + "walkdir", ] [[package]] diff --git a/aptos-move/aptos-release-builder/Cargo.toml b/aptos-move/aptos-release-builder/Cargo.toml index a42cff88383d2..2d0c50b4750dd 100644 --- a/aptos-move/aptos-release-builder/Cargo.toml +++ b/aptos-move/aptos-release-builder/Cargo.toml @@ -24,18 +24,26 @@ aptos-gas-schedule-updator = { workspace = true } aptos-genesis = { workspace = true } aptos-infallible = { workspace = true } aptos-keygen = { workspace = true } +aptos-language-e2e-tests = { workspace = true } +aptos-move-debugger = { workspace = true } aptos-rest-client = { workspace = true } aptos-temppath = { workspace = true } aptos-types = { workspace = true } +aptos-vm = { workspace = true } +aptos-vm-logging = { workspace = true } bcs = { workspace = true } clap = { workspace = true } futures = { workspace = true } git2 = { workspace = true } handlebars = { workspace = true } hex = { workspace = true } +move-binary-format = { workspace = true } +move-bytecode-verifier = { workspace = true } move-core-types = { workspace = true } move-model = { workspace = true } +move-vm-types = { workspace = true } once_cell = { workspace = true } +parking_lot = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -45,6 +53,7 @@ strum = { workspace = true } strum_macros = { workspace = true } tokio = { workspace = true } url = { workspace = true } +walkdir = { workspace = true } [[bin]] name = "aptos-release-builder" diff --git a/aptos-move/aptos-release-builder/data/release.yaml b/aptos-move/aptos-release-builder/data/release.yaml index 13afcdcbbbe77..8bb47bdcbe35f 100644 --- a/aptos-move/aptos-release-builder/data/release.yaml +++ b/aptos-move/aptos-release-builder/data/release.yaml @@ -1,19 +1,15 @@ --- -remote_endpoint: ~ -name: "v1.14" +remote_endpoint: https://fullnode.mainnet.aptoslabs.com +name: "TBD" proposals: - - name: step_1_upgrade_framework + - name: proposal_1_upgrade_framework metadata: - title: "Multi-step proposal to upgrade mainnet framework to v1.14" - description: "This includes changes in https://github.com/aptos-labs/aptos-core/commits/aptos-release-v1.13" + title: "Multi-step proposal to upgrade mainnet framework, version TBD" + description: "This includes changes in (TBA: URL to changes)" execution_mode: MultiStep update_sequence: + - Gas: + new: current - Framework: bytecode_version: 6 git_hash: ~ - - Gas: - new: current - - FeatureFlag: - enabled: - - disallow_user_native - diff --git a/aptos-move/aptos-release-builder/src/lib.rs b/aptos-move/aptos-release-builder/src/lib.rs index b53dd7006f106..2ac6a94f9003e 100644 --- a/aptos-move/aptos-release-builder/src/lib.rs +++ b/aptos-move/aptos-release-builder/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod components; +pub mod simulate; mod utils; pub mod validate; diff --git a/aptos-move/aptos-release-builder/src/main.rs b/aptos-move/aptos-release-builder/src/main.rs index 205e8d8224621..8352800e81cd7 100644 --- a/aptos-move/aptos-release-builder/src/main.rs +++ b/aptos-move/aptos-release-builder/src/main.rs @@ -8,6 +8,7 @@ use aptos_gas_schedule::LATEST_GAS_FEATURE_VERSION; use aptos_release_builder::{ components::fetch_config, initialize_aptos_core_path, + simulate::simulate_all_proposals, validate::{DEFAULT_RESOLUTION_TIME, FAST_RESOLUTION_TIME}, }; use aptos_types::{ @@ -17,6 +18,7 @@ use aptos_types::{ }; use clap::{Parser, Subcommand}; use std::{path::PathBuf, str::FromStr}; +use url::Url; #[derive(Parser)] pub struct Argument { @@ -26,6 +28,43 @@ pub struct Argument { aptos_core_path: Option, } +// TODO(vgao1996): unify with `ReplayNetworkSelection` in the `aptos` crate. +#[derive(Clone, Debug)] +pub enum NetworkSelection { + Mainnet, + Testnet, + Devnet, + RestEndpoint(String), +} + +impl FromStr for NetworkSelection { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "mainnet" => Self::Mainnet, + "testnet" => Self::Testnet, + "devnet" => Self::Devnet, + _ => Self::RestEndpoint(s.to_owned()), + }) + } +} + +impl NetworkSelection { + fn to_url(&self) -> anyhow::Result { + use NetworkSelection::*; + + let s = match &self { + Mainnet => "https://fullnode.mainnet.aptoslabs.com", + Testnet => "https://fullnode.testnet.aptoslabs.com", + Devnet => "https://fullnode.devnet.aptoslabs.com", + RestEndpoint(url) => url, + }; + + Ok(Url::parse(s)?) + } +} + #[derive(Subcommand, Debug)] pub enum Commands { /// Generate sets of governance proposals based on the release_config file passed in @@ -34,6 +73,24 @@ pub enum Commands { release_config: PathBuf, #[clap(short, long)] output_dir: PathBuf, + + #[clap(long)] + simulate: Option, + }, + /// Simulate a multi-step proposal on the specified network, using its current states. + /// The simulation will execute the governance scripts, as if the proposal is already + /// approved. + Simulate { + /// Directory that may contain one or more proposals at any level + /// within its sub-directory hierarchy. + #[clap(short, long)] + path: PathBuf, + + /// The network to simulate on. + /// + /// Possible values: devnet, testnet, mainnet, + #[clap(long)] + network: NetworkSelection, }, /// Generate sets of governance proposals with default release config. WriteDefault { @@ -126,12 +183,23 @@ async fn main() -> anyhow::Result<()> { Commands::GenerateProposals { release_config, output_dir, + simulate, } => { aptos_release_builder::ReleaseConfig::load_config(release_config.as_path()) .with_context(|| "Failed to load release config".to_string())? .generate_release_proposal_scripts(output_dir.as_path()) .await .with_context(|| "Failed to generate release proposal scripts".to_string())?; + + if let Some(network) = simulate { + let remote_endpoint = network.to_url()?; + simulate_all_proposals(remote_endpoint, output_dir.as_path()).await?; + } + + Ok(()) + }, + Commands::Simulate { network, path } => { + simulate_all_proposals(network.to_url()?, &path).await?; Ok(()) }, Commands::WriteDefault { output_path } => { diff --git a/aptos-move/aptos-release-builder/src/simulate.rs b/aptos-move/aptos-release-builder/src/simulate.rs new file mode 100644 index 0000000000000..818843b4392f8 --- /dev/null +++ b/aptos-move/aptos-release-builder/src/simulate.rs @@ -0,0 +1,685 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +//! This module implements the simulation of governance proposals. +//! Currently, it supports only multi-step proposals. +//! +//! It utilizes the remote debugger infrastructure to fetch real chain states +//! for local simulation, but adds another in-memory database to store the new side effects +//! generated by the governance scripts. +//! +//! Normally, governance scripts needs to be approved through on-chain governance +//! before they could be executed. This process involves setting up various states +//! (e.g., staking pool, delegated voter), which can be quite complex. +//! +//! This simulation bypasses these challenges by patching specific Move functions +//! with mock versions, most notably `fun resolve_multi_step_proposal`, thus allowing +//! the governance process to be skipped altogether. +//! +//! In other words, this simulation is intended for checking whether a governance +//! proposal will execute successfully, assuming it gets approved, not whether the +//! governance framework itself is working as intended. + +use crate::aptos_framework_path; +use anyhow::{anyhow, bail, Context, Result}; +use aptos::{ + common::types::PromptOptions, governance::compile_in_temp_dir, move_tool::FrameworkPackageArgs, +}; +use aptos_crypto::HashValue; +use aptos_gas_schedule::{AptosGasParameters, FromOnChainGasSchedule}; +use aptos_language_e2e_tests::account::AccountData; +use aptos_move_debugger::aptos_debugger::AptosDebugger; +use aptos_rest_client::Client; +use aptos_types::{ + account_address::AccountAddress, + account_config::ChainIdResource, + on_chain_config::{ApprovedExecutionHashes, Features, GasScheduleV2, OnChainConfig}, + state_store::{ + in_memory_state_view::InMemoryStateView, state_key::StateKey, + state_storage_usage::StateStorageUsage, state_value::StateValue, + Result as StateStoreResult, StateView, TStateView, + }, + transaction::{ExecutionStatus, Script, TransactionArgument, TransactionStatus}, + vm::configs::aptos_prod_deserializer_config, + write_set::{TransactionWrite, WriteSet}, +}; +use aptos_vm::{data_cache::AsMoveResolver, move_vm_ext::flush_warm_vm_cache, AptosVM}; +use aptos_vm_logging::log_schema::AdapterLogSchema; +use clap::Parser; +use move_binary_format::{ + access::ModuleAccess, + deserializer::DeserializerConfig, + file_format::{ + AddressIdentifierIndex, Bytecode, FunctionDefinition, FunctionHandle, FunctionHandleIndex, + IdentifierIndex, ModuleHandle, ModuleHandleIndex, Signature, SignatureIndex, + SignatureToken, Visibility, + }, + CompiledModule, +}; +use move_core_types::{ + identifier::{IdentStr, Identifier}, + language_storage::{ModuleId, StructTag}, + move_resource::MoveResource, +}; +use move_vm_types::resolver::ModuleResolver; +use once_cell::sync::Lazy; +use parking_lot::Mutex; +use serde::Serialize; +use std::{ + collections::HashMap, + io::Write, + path::{Path, PathBuf}, +}; +use url::Url; +use walkdir::WalkDir; + +/*************************************************************************************************** + * Compiled Module Helpers + * + **************************************************************************************************/ +fn find_function_def_by_name<'a>( + m: &'a mut CompiledModule, + name: &IdentStr, +) -> Option<&'a mut FunctionDefinition> { + for (idx, func_def) in m.function_defs.iter().enumerate() { + let func_handle = m.function_handle_at(func_def.function); + let func_name = m.identifier_at(func_handle.name); + if name == func_name { + return Some(&mut m.function_defs[idx]); + } + } + None +} + +fn get_or_add(pool: &mut Vec, val: T) -> usize { + match pool.iter().position(|elem| elem == &val) { + Some(idx) => idx, + None => { + let idx = pool.len(); + pool.push(val); + idx + }, + } +} + +#[allow(dead_code)] +fn get_or_add_addr(m: &mut CompiledModule, addr: AccountAddress) -> AddressIdentifierIndex { + AddressIdentifierIndex::new(get_or_add(&mut m.address_identifiers, addr) as u16) +} + +fn get_or_add_ident(m: &mut CompiledModule, ident: Identifier) -> IdentifierIndex { + IdentifierIndex::new(get_or_add(&mut m.identifiers, ident) as u16) +} + +#[allow(dead_code)] +fn get_or_add_module_handle( + m: &mut CompiledModule, + addr: AccountAddress, + name: Identifier, +) -> ModuleHandleIndex { + let addr = get_or_add_addr(m, addr); + let name = get_or_add_ident(m, name); + let module_handle = ModuleHandle { + address: addr, + name, + }; + ModuleHandleIndex::new(get_or_add(&mut m.module_handles, module_handle) as u16) +} + +fn get_or_add_signature(m: &mut CompiledModule, sig: Vec) -> SignatureIndex { + SignatureIndex::new(get_or_add(&mut m.signatures, Signature(sig)) as u16) +} + +fn find_function_handle_by_name( + m: &CompiledModule, + addr: AccountAddress, + module_name: &IdentStr, + func_name: &IdentStr, +) -> Option { + for (idx, func_handle) in m.function_handles().iter().enumerate() { + let module_handle = m.module_handle_at(func_handle.module); + if m.address_identifier_at(module_handle.address) == &addr + && m.identifier_at(module_handle.name) == module_name + && m.identifier_at(func_handle.name) == func_name + { + return Some(FunctionHandleIndex(idx as u16)); + } + } + None +} + +fn add_simple_native_function( + m: &mut CompiledModule, + func_name: Identifier, + params: Vec, + returns: Vec, +) -> Result { + if let Some(func_handle_idx) = + find_function_handle_by_name(m, *m.self_addr(), m.self_name(), &func_name) + { + return Ok(func_handle_idx); + } + + let name = get_or_add_ident(m, func_name); + let parameters = get_or_add_signature(m, params); + let return_ = get_or_add_signature(m, returns); + let func_handle = FunctionHandle { + module: m.self_handle_idx(), + name, + parameters, + return_, + type_parameters: vec![], + access_specifiers: None, + }; + let func_handle_idx = FunctionHandleIndex(m.function_handles.len() as u16); + m.function_handles.push(func_handle); + + let func_def = FunctionDefinition { + function: func_handle_idx, + visibility: Visibility::Private, + is_entry: false, + acquires_global_resources: vec![], + code: None, + }; + m.function_defs.push(func_def); + + Ok(func_handle_idx) +} +/*************************************************************************************************** + * Simulation State View + * + **************************************************************************************************/ +/// A state view specifically designed for managing the side effects generated by +/// the governance scripts. +/// +/// It comprises two components: +/// - A remote debugger state view to enable on-demand data fetching. +/// - A local state store to allow new changes to be stacked on top of the remote state. +struct SimulationStateView<'a, S> { + remote: &'a S, + states: Mutex>>, +} + +impl<'a, S> SimulationStateView<'a, S> +where + S: StateView, +{ + fn set_state_value(&self, state_key: StateKey, state_val: StateValue) { + self.states.lock().insert(state_key, Some(state_val)); + } + + fn set_on_chain_config(&self, config: &C) -> Result<()> + where + C: OnChainConfig + Serialize, + { + let addr = AccountAddress::from_hex_literal(C::ADDRESS).unwrap(); + + self.set_state_value( + StateKey::resource(&addr, &StructTag { + address: addr, + module: Identifier::new(C::MODULE_IDENTIFIER).unwrap(), + name: Identifier::new(C::TYPE_IDENTIFIER).unwrap(), + type_args: vec![], + })?, + StateValue::new_legacy(bcs::to_bytes(&config)?.into()), + ); + + Ok(()) + } + + fn modify_on_chain_config(&self, modify: F) -> Result<()> + where + C: OnChainConfig + Serialize, + F: FnOnce(&mut C) -> Result<()>, + { + let mut config = C::fetch_config(self).ok_or_else(|| { + anyhow!( + "failed to fetch on-chain config: {:?}", + std::any::type_name::() + ) + })?; + + modify(&mut config)?; + + self.set_on_chain_config(&config)?; + + Ok(()) + } + + #[allow(dead_code)] + fn remove_state_value(&mut self, state_key: &StateKey) { + self.states.lock().remove(state_key); + } + + fn apply_write_set(&self, write_set: WriteSet) { + let mut states = self.states.lock(); + + for (state_key, write_op) in write_set { + match write_op.as_state_value() { + None => { + states.remove(&state_key); + }, + Some(state_val) => { + states.insert(state_key, Some(state_val)); + }, + } + } + } + + #[allow(dead_code)] + fn read_resource(&self, addr: &AccountAddress) -> T { + let data_blob = self + .get_state_value_bytes( + &StateKey::resource_typed::(addr).expect("failed to create StateKey"), + ) + .expect("account must exist in data store") + .unwrap_or_else(|| panic!("Can't fetch {} resource for {}", T::STRUCT_NAME, addr)); + + bcs::from_bytes(&data_blob).expect("failed to deserialize resource") + } +} + +impl<'a, S> TStateView for SimulationStateView<'a, S> +where + S: StateView, +{ + type Key = StateKey; + + fn get_state_value(&self, state_key: &Self::Key) -> StateStoreResult> { + if let Some(res) = self.states.lock().get(state_key) { + return Ok(res.clone()); + } + self.remote.get_state_value(state_key) + } + + fn get_usage(&self) -> StateStoreResult { + panic!("not supported") + } + + fn as_in_memory_state_view(&self) -> InMemoryStateView { + panic!("not supported") + } +} + +/*************************************************************************************************** + * Patches + * + **************************************************************************************************/ +static MODULE_ID_APTOS_GOVERNANCE: Lazy = Lazy::new(|| { + ModuleId::new( + AccountAddress::ONE, + Identifier::new("aptos_governance").unwrap(), + ) +}); + +static FUNC_NAME_CREATE_SIGNER: Lazy = + Lazy::new(|| Identifier::new("create_signer").unwrap()); + +static FUNC_NAME_RESOLVE_MULTI_STEP_PROPOSAL: Lazy = + Lazy::new(|| Identifier::new("resolve_multi_step_proposal").unwrap()); + +const DUMMY_PROPOSAL_ID: u64 = u64::MAX; + +const MAGIC_FAILED_NEXT_EXECUTION_HASH_CHECK: u64 = 0xDEADBEEF; + +/// Helper to load a module from the state view, deserialize it, modify it with +/// the provided callback, reserialize it and finally write it back. +fn patch_module( + state_view: &SimulationStateView, + deserializer_config: &DeserializerConfig, + module_id: &ModuleId, + modify_module: F, +) -> Result<()> +where + F: FnOnce(&mut CompiledModule) -> Result<()>, +{ + let resolver = state_view.as_move_resolver(); + let blob = resolver + .get_module(module_id)? + .ok_or_else(|| anyhow!("module {} does not exist", module_id))?; + + let mut m = CompiledModule::deserialize_with_config(&blob, deserializer_config)?; + + modify_module(&mut m)?; + + // Sanity check to ensure the correctness of the check + move_bytecode_verifier::verify_module(&m).map_err(|err| { + anyhow!( + "patched module failed to verify -- check if the patch is correct: {}", + err + ) + })?; + + let mut blob = vec![]; + m.serialize(&mut blob)?; + + state_view.set_state_value( + StateKey::module_id(module_id), + StateValue::new_legacy(blob.into()), + ); + + Ok(()) +} + +/// Patches `aptos_framework::aptos_governance::resolve_multi_step_proposal` so that +/// it returns the requested signer directly, skipping the governance process altogether. +fn patch_aptos_governance( + state_view: &SimulationStateView, + deserializer_config: &DeserializerConfig, + forbid_next_execution_hash: bool, +) -> Result<()> { + use Bytecode::*; + + patch_module( + state_view, + deserializer_config, + &MODULE_ID_APTOS_GOVERNANCE, + |m| { + // Inject `native fun create_signer`. + let create_signer_handle_idx = add_simple_native_function( + m, + FUNC_NAME_CREATE_SIGNER.clone(), + vec![SignatureToken::Address], + vec![SignatureToken::Signer], + )?; + + // Patch `fun resolve_multi_step_proposal`. + let sig_u8_idx = get_or_add_signature(m, vec![SignatureToken::U8]); + + let func_def = find_function_def_by_name(m, &FUNC_NAME_RESOLVE_MULTI_STEP_PROPOSAL) + .ok_or_else(|| { + anyhow!( + "failed to locate `fun {}`", + &*FUNC_NAME_RESOLVE_MULTI_STEP_PROPOSAL + ) + })?; + func_def.acquires_global_resources = vec![]; + let code = func_def.code.as_mut().ok_or_else(|| { + anyhow!( + "`fun {}` must have a Move-defined body", + &*FUNC_NAME_RESOLVE_MULTI_STEP_PROPOSAL + ) + })?; + + code.code.clear(); + if forbid_next_execution_hash { + // If it is needed to forbid a next execution hash, inject additional Move + // code at the beginning that aborts with a magic number if the vector + // representing the hash is not empty. + // + // if (!vector::is_empty(&next_execution_hash)) { + // abort MAGIC_FAILED_NEXT_EXECUTION_HASH_CHECK; + // } + // + // The magic number can later be checked in Rust to determine if such violation + // has happened. + code.code.extend([ + ImmBorrowLoc(2), + VecLen(sig_u8_idx), + LdU64(0), + Eq, + BrTrue(7), + LdU64(MAGIC_FAILED_NEXT_EXECUTION_HASH_CHECK), + Abort, + ]); + } + // Replace the original logic with `create_signer(signer_address)`, bypassing + // the governance process. + code.code + .extend([MoveLoc(1), Call(create_signer_handle_idx), Ret]); + + Ok(()) + }, + ) +} + +// Add the hash of the script to the list of approved hashes, so to enable the +// alternative (higher) execution limits. +fn add_script_execution_hash( + state_view: &SimulationStateView, + hash: HashValue, +) -> Result<()> { + let entry = (DUMMY_PROPOSAL_ID, hash.to_vec()); + + state_view.modify_on_chain_config(|approved_hashes: &mut ApprovedExecutionHashes| { + if !approved_hashes.entries.contains(&entry) { + approved_hashes.entries.push(entry); + } + Ok(()) + }) +} + +/*************************************************************************************************** + * Simulation Workflow + * + **************************************************************************************************/ +pub async fn simulate_multistep_proposal( + remote_url: Url, + proposal_dir: &Path, + proposal_scripts: &[PathBuf], +) -> Result<()> { + println!("Simulating proposal at {}", proposal_dir.display()); + + // Compile all scripts. + println!("Compiling scripts..."); + let mut compiled_scripts = vec![]; + for path in proposal_scripts { + let framework_package_args = FrameworkPackageArgs::try_parse_from([ + "dummy_executable_name", + "--framework-local-dir", + &aptos_framework_path().to_string_lossy(), + "--skip-fetch-latest-git-deps", + ]) + .context( + "failed to parse framework package args for compiling scripts, this should not happen", + )?; + + let (blob, hash) = compile_in_temp_dir( + "script", + path, + &framework_package_args, + PromptOptions::yes(), + None, + ) + .with_context(|| format!("failed to compile script {}", path.display()))?; + + compiled_scripts.push((blob, hash)); + } + + // Set up the simulation state view. + let client = Client::new(remote_url); + let debugger = + AptosDebugger::rest_client(client.clone()).context("failed to create AptosDebugger")?; + let state = client.get_ledger_information().await?.into_inner(); + + let state_view = SimulationStateView { + remote: &debugger.state_view_at_version(state.version), + states: Mutex::new(HashMap::new()), + }; + + // Create and fund a sender account that is used to send the governance scripts. + print!("Creating and funding sender account.. "); + std::io::stdout().flush()?; + let mut rng = aptos_keygen::KeyGen::from_seed([0; 32]); + let balance = 100 * 1_0000_0000; // 100 APT + let account = AccountData::new_from_seed(&mut rng, balance, 0); + state_view.apply_write_set(account.to_writeset()); + // TODO: should update coin info (total supply) + println!("done"); + + // Execute the governance scripts in sorted order. + println!("Executing governance scripts..."); + + for (script_idx, (script_path, (script_blob, script_hash))) in + proposal_scripts.iter().zip(compiled_scripts).enumerate() + { + // Fetch the on-chain configs that are needed for the simulation. + let chain_id = + ChainIdResource::fetch_config(&state_view).context("failed to fetch chain id")?; + + let gas_schedule = + GasScheduleV2::fetch_config(&state_view).context("failed to fetch gas schedule v2")?; + let gas_feature_version = gas_schedule.feature_version; + let gas_params = AptosGasParameters::from_on_chain_gas_schedule( + &gas_schedule.into_btree_map(), + gas_feature_version, + ) + .map_err(|err| { + anyhow!( + "failed to construct gas params at gas version {}: {}", + gas_feature_version, + err + ) + })?; + + // Patch framework functions to skip the governance process. + // This is redone every time we execute a script because the previous script could have + // overwritten the framework. + let features = + Features::fetch_config(&state_view).context("failed to fetch feature flags")?; + let deserializer_config = aptos_prod_deserializer_config(&features); + + // If the script is the last step of the proposal, it MUST NOT have a next execution hash. + // Set the boolean flag to true to use a modified patch to catch this. + let forbid_next_execution_hash = script_idx == proposal_scripts.len() - 1; + patch_aptos_governance( + &state_view, + &deserializer_config, + forbid_next_execution_hash, + ) + .context("failed to patch resolve_multistep_proposal")?; + + // Add the hash of the script to the list of approved hashes, so that the + // alternative (usually higher) execution limits can be used. + add_script_execution_hash(&state_view, script_hash) + .context("failed to add script execution hash")?; + + let script_name = script_path.file_name().unwrap().to_string_lossy(); + println!(" {}", script_name); + + // Create a new VM to ensure the loader is clean. + // The warm vm cache also needs to be explicitly flushed as it cannot detect the + // patches we performed. + flush_warm_vm_cache(); + let vm = AptosVM::new_for_gov_sim(&state_view); + let log_context = AdapterLogSchema::new(state_view.id(), 0); + let resolver = state_view.as_move_resolver(); + let (_vm_status, vm_output) = vm.execute_user_transaction( + &resolver, + &account + .account() + .transaction() + .script(Script::new(script_blob, vec![], vec![ + TransactionArgument::U64(DUMMY_PROPOSAL_ID), // dummy proposal id, ignored by the patched function + ])) + .chain_id(chain_id.chain_id()) + .sequence_number(script_idx as u64) + .gas_unit_price(gas_params.vm.txn.min_price_per_gas_unit.into()) + .max_gas_amount(100000) + .ttl(u64::MAX) + .sign(), + &log_context, + ); + // TODO: ensure all scripts trigger reconfiguration. + + let txn_output = vm_output + .try_materialize_into_transaction_output(&resolver) + .context("failed to materialize transaction output")?; + let txn_status = txn_output.status(); + match txn_status { + TransactionStatus::Keep(ExecutionStatus::Success) => { + println!(" Success") + }, + TransactionStatus::Keep(ExecutionStatus::MoveAbort { code, .. }) + if *code == MAGIC_FAILED_NEXT_EXECUTION_HASH_CHECK => + { + bail!("the last script has a non-zero next execution hash") + }, + _ => { + println!( + "{}", + format!("{:#?}", txn_status) + .lines() + .map(|line| format!(" {}", line)) + .collect::>() + .join("\n") + ); + bail!("failed to execute governance script: {}", script_name) + }, + } + + let (write_set, _events) = txn_output.into(); + state_view.apply_write_set(write_set); + } + + println!("All scripts succeeded!"); + + Ok(()) +} + +pub fn collect_proposals(root_dir: &Path) -> Result)>> { + let mut result = Vec::new(); + + for entry in WalkDir::new(root_dir) { + let entry = entry.unwrap(); + if entry.path().is_dir() { + let sub_dir = entry.path(); + let mut move_files = Vec::new(); + + for sub_entry in WalkDir::new(sub_dir).min_depth(1).max_depth(1) { + let sub_entry = sub_entry.unwrap(); + if sub_entry.path().is_file() + && sub_entry.path().extension() == Some(std::ffi::OsStr::new("move")) + { + move_files.push(sub_entry.path().to_path_buf()); + } + } + + if !move_files.is_empty() { + move_files.sort(); + result.push((sub_dir.to_path_buf(), move_files)); + } + } + } + + result.sort_by(|(path1, _), (path2, _)| path1.cmp(path2)); + + Ok(result) +} + +pub async fn simulate_all_proposals(remote_url: Url, output_dir: &Path) -> Result<()> { + let proposals = + collect_proposals(output_dir).context("failed to collect proposals for simulation")?; + + if proposals.is_empty() { + bail!("failed to simulate proposals: no proposals found") + } + + println!( + "Found {} proposal{}", + proposals.len(), + if proposals.len() == 1 { "" } else { "s" } + ); + for (proposal_dir, proposal_scripts) in &proposals { + println!(" {}", proposal_dir.display()); + + for script_path in proposal_scripts { + println!( + " {}", + script_path.file_name().unwrap().to_string_lossy() + ); + } + } + + for (proposal_dir, proposal_scripts) in &proposals { + simulate_multistep_proposal(remote_url.clone(), proposal_dir, proposal_scripts) + .await + .with_context(|| { + format!("failed to simulate proposal at {}", proposal_dir.display()) + })?; + } + + println!("All proposals succeeded!"); + + Ok(()) +} diff --git a/aptos-move/aptos-vm/src/aptos_vm.rs b/aptos-move/aptos-vm/src/aptos_vm.rs index bcb6f2e5462c7..b5ac677023c91 100644 --- a/aptos-move/aptos-vm/src/aptos_vm.rs +++ b/aptos-move/aptos-vm/src/aptos_vm.rs @@ -219,21 +219,37 @@ impl AptosVM { /// Creates a new VM instance, initializing the runtime environment from the state. pub fn new(state_view: &impl StateView) -> Self { let env = Arc::new(Environment::new(state_view)); - Self::new_with_environment(env, state_view) + Self::new_with_environment(env, state_view, false) + } + + pub fn new_for_gov_sim(state_view: &impl StateView) -> Self { + let env = Arc::new(Environment::new(state_view)); + Self::new_with_environment(env, state_view, true) } /// Creates a new VM instance based on the runtime environment, and used by block /// executor to create multiple tasks sharing the same execution configurations. // TODO: Passing `state_view` is not needed once we move keyless and gas-related // configs to the environment. - pub(crate) fn new_with_environment(env: Arc, state_view: &impl StateView) -> Self { + pub(crate) fn new_with_environment( + env: Arc, + state_view: &impl StateView, + inject_create_signer_for_gov_sim: bool, + ) -> Self { let _timer = TIMER.timer_with(&["AptosVM::new"]); let (gas_params, storage_gas_params, gas_feature_version) = get_gas_parameters(env.features(), state_view); let resolver = state_view.as_move_resolver(); - let move_vm = MoveVmExt::new(gas_feature_version, gas_params.as_ref(), env, &resolver); + let move_vm = MoveVmExt::new_with_extended_options( + gas_feature_version, + gas_params.as_ref(), + env, + None, + inject_create_signer_for_gov_sim, + &resolver, + ); // We use an `Option` to handle the VK not being set on-chain, or an incorrect VK being set // via governance (although, currently, we do check for that in `keyless_account.move`). diff --git a/aptos-move/aptos-vm/src/block_executor/vm_wrapper.rs b/aptos-move/aptos-vm/src/block_executor/vm_wrapper.rs index ea86fcd0881a6..058ea9de2553d 100644 --- a/aptos-move/aptos-vm/src/block_executor/vm_wrapper.rs +++ b/aptos-move/aptos-vm/src/block_executor/vm_wrapper.rs @@ -33,7 +33,7 @@ impl ExecutorTask for AptosExecutorTask { type Txn = SignatureVerifiedTransaction; fn init(env: Self::Environment, state_view: &impl StateView) -> Self { - let vm = AptosVM::new_with_environment(env, state_view); + let vm = AptosVM::new_with_environment(env, state_view, false); let id = state_view.id(); Self { vm, id } } diff --git a/aptos-move/aptos-vm/src/move_vm_ext/mod.rs b/aptos-move/aptos-vm/src/move_vm_ext/mod.rs index bcd7a1f381d1e..7348a280dffdf 100644 --- a/aptos-move/aptos-vm/src/move_vm_ext/mod.rs +++ b/aptos-move/aptos-vm/src/move_vm_ext/mod.rs @@ -21,6 +21,7 @@ use move_core_types::{ account_address::AccountAddress, language_storage::StructTag, vm_status::StatusCode, }; pub use session::session_id::SessionId; +pub use warm_vm_cache::flush_warm_vm_cache; pub(crate) fn resource_state_key( address: &AccountAddress, diff --git a/aptos-move/aptos-vm/src/move_vm_ext/vm.rs b/aptos-move/aptos-vm/src/move_vm_ext/vm.rs index 443829ee16ef2..880207ec09d4a 100644 --- a/aptos-move/aptos-vm/src/move_vm_ext/vm.rs +++ b/aptos-move/aptos-vm/src/move_vm_ext/vm.rs @@ -60,7 +60,7 @@ impl GenesisMoveVM { ); let vm = MoveVM::new_with_config( - aptos_natives_with_builder(&mut native_builder), + aptos_natives_with_builder(&mut native_builder, false), vm_config.clone(), ); @@ -105,6 +105,7 @@ impl MoveVmExt { gas_params: Result<&AptosGasParameters, &String>, env: Arc, gas_hook: Option>, + inject_create_signer_for_gov_sim: bool, resolver: &impl AptosMoveResolver, ) -> Self { // TODO(Gas): Right now, we have to use some dummy values for gas parameters if they are not found on-chain. @@ -155,6 +156,7 @@ impl MoveVmExt { vm_config, resolver, env.features().is_enabled(FeatureFlag::VM_BINARY_FORMAT_V7), + inject_create_signer_for_gov_sim, ) .expect("should be able to create Move VM; check if there are duplicated natives"), env, @@ -167,17 +169,25 @@ impl MoveVmExt { env: Arc, resolver: &impl AptosMoveResolver, ) -> Self { - Self::new_impl(gas_feature_version, gas_params, env, None, resolver) + Self::new_impl(gas_feature_version, gas_params, env, None, false, resolver) } - pub fn new_with_gas_hook( + pub fn new_with_extended_options( gas_feature_version: u64, gas_params: Result<&AptosGasParameters, &String>, env: Arc, gas_hook: Option>, + inject_create_signer_for_gov_sim: bool, resolver: &impl AptosMoveResolver, ) -> Self { - Self::new_impl(gas_feature_version, gas_params, env, gas_hook, resolver) + Self::new_impl( + gas_feature_version, + gas_params, + env, + gas_hook, + inject_create_signer_for_gov_sim, + resolver, + ) } pub fn new_session<'r, R: AptosMoveResolver>( diff --git a/aptos-move/aptos-vm/src/move_vm_ext/warm_vm_cache.rs b/aptos-move/aptos-vm/src/move_vm_ext/warm_vm_cache.rs index d7f58bff2b542..5f7b47ca41edc 100644 --- a/aptos-move/aptos-vm/src/move_vm_ext/warm_vm_cache.rs +++ b/aptos-move/aptos-vm/src/move_vm_ext/warm_vm_cache.rs @@ -30,14 +30,25 @@ static WARM_VM_CACHE: Lazy = Lazy::new(|| WarmVmCache { cache: RwLock::new(HashMap::new()), }); +pub fn flush_warm_vm_cache() { + WARM_VM_CACHE.cache.write().clear(); +} + impl WarmVmCache { pub(crate) fn get_warm_vm( native_builder: SafeNativeBuilder, vm_config: VMConfig, resolver: &impl AptosMoveResolver, bin_v7_enabled: bool, + inject_create_signer_for_gov_sim: bool, ) -> VMResult { - WARM_VM_CACHE.get(native_builder, vm_config, resolver, bin_v7_enabled) + WARM_VM_CACHE.get( + native_builder, + vm_config, + resolver, + bin_v7_enabled, + inject_create_signer_for_gov_sim, + ) } fn get( @@ -46,11 +57,18 @@ impl WarmVmCache { vm_config: VMConfig, resolver: &impl AptosMoveResolver, bin_v7_enabled: bool, + inject_create_signer_for_gov_sim: bool, ) -> VMResult { let _timer = TIMER.timer_with(&["warm_vm_get"]); let id = { let _timer = TIMER.timer_with(&["get_warm_vm_id"]); - WarmVmId::new(&native_builder, &vm_config, resolver, bin_v7_enabled)? + WarmVmId::new( + &native_builder, + &vm_config, + resolver, + bin_v7_enabled, + inject_create_signer_for_gov_sim, + )? }; if let Some(vm) = self.cache.read().get(&id) { @@ -66,8 +84,10 @@ impl WarmVmCache { return Ok(vm.clone()); } - let vm = - MoveVM::new_with_config(aptos_natives_with_builder(&mut native_builder), vm_config); + let vm = MoveVM::new_with_config( + aptos_natives_with_builder(&mut native_builder, inject_create_signer_for_gov_sim), + vm_config, + ); Self::warm_vm_up(&vm, resolver); // Not using LruCache because its `::get()` requires &mut self @@ -102,6 +122,7 @@ struct WarmVmId { vm_config: Bytes, core_packages_registry: Option, bin_v7_enabled: bool, + inject_create_signer_for_gov_sim: bool, } impl WarmVmId { @@ -110,6 +131,7 @@ impl WarmVmId { vm_config: &VMConfig, resolver: &impl AptosMoveResolver, bin_v7_enabled: bool, + inject_create_signer_for_gov_sim: bool, ) -> VMResult { let natives = { let _timer = TIMER.timer_with(&["serialize_native_builder"]); @@ -120,6 +142,7 @@ impl WarmVmId { vm_config: Self::vm_config_bytes(vm_config), core_packages_registry: Self::core_packages_id_bytes(resolver)?, bin_v7_enabled, + inject_create_signer_for_gov_sim, }) } diff --git a/aptos-move/aptos-vm/src/natives.rs b/aptos-move/aptos-vm/src/natives.rs index b3732c97507ad..4fb5a39b11273 100644 --- a/aptos-move/aptos-vm/src/natives.rs +++ b/aptos-move/aptos-vm/src/natives.rs @@ -162,10 +162,13 @@ pub fn aptos_natives( None, ); - aptos_natives_with_builder(&mut builder) + aptos_natives_with_builder(&mut builder, false) } -pub fn aptos_natives_with_builder(builder: &mut SafeNativeBuilder) -> NativeFunctionTable { +pub fn aptos_natives_with_builder( + builder: &mut SafeNativeBuilder, + inject_create_signer_for_gov_sim: bool, +) -> NativeFunctionTable { #[allow(unreachable_code)] aptos_move_stdlib::natives::all_natives(CORE_CODE_ADDRESS, builder) .into_iter() @@ -173,6 +176,7 @@ pub fn aptos_natives_with_builder(builder: &mut SafeNativeBuilder) -> NativeFunc .chain(aptos_framework::natives::all_natives( CORE_CODE_ADDRESS, builder, + inject_create_signer_for_gov_sim, )) .chain(aptos_table_natives::table_natives( CORE_CODE_ADDRESS, diff --git a/aptos-move/e2e-tests/src/executor.rs b/aptos-move/e2e-tests/src/executor.rs index 1e00807b31264..3ef32daeba35b 100644 --- a/aptos-move/e2e-tests/src/executor.rs +++ b/aptos-move/e2e-tests/src/executor.rs @@ -977,13 +977,14 @@ impl FakeExecutor { let resolver = self.data_store.as_move_resolver(); // TODO(Gas): we probably want to switch to non-zero costs in the future - let vm = MoveVmExt::new_with_gas_hook( + let vm = MoveVmExt::new_with_extended_options( LATEST_GAS_FEATURE_VERSION, Ok(&AptosGasParameters::zeros()), self.env.clone(), Some(Arc::new(move |expression| { a2.lock().unwrap().push(expression); })), + false, &resolver, ); let mut session = vm.new_session(&resolver, SessionId::void(), None); diff --git a/aptos-move/framework/src/natives/mod.rs b/aptos-move/framework/src/natives/mod.rs index bec070b996540..dcfc80407f932 100644 --- a/aptos-move/framework/src/natives/mod.rs +++ b/aptos-move/framework/src/natives/mod.rs @@ -24,7 +24,7 @@ pub mod util; use crate::natives::cryptography::multi_ed25519; use aggregator_natives::{aggregator, aggregator_factory, aggregator_v2}; -use aptos_native_interface::SafeNativeBuilder; +use aptos_native_interface::{RawSafeNative, SafeNativeBuilder}; use cryptography::ed25519; use move_core_types::account_address::AccountAddress; use move_vm_runtime::native_functions::{make_table_from_iter, NativeFunctionTable}; @@ -39,6 +39,7 @@ pub mod status { pub fn all_natives( framework_addr: AccountAddress, builder: &SafeNativeBuilder, + inject_create_signer_for_gov_sim: bool, ) -> NativeFunctionTable { let mut natives = vec![]; @@ -91,5 +92,15 @@ pub fn all_natives( dispatchable_fungible_asset::make_all(builder) ); + if inject_create_signer_for_gov_sim { + add_natives_from_module!( + "aptos_governance", + builder.make_named_natives([( + "create_signer", + create_signer::native_create_signer as RawSafeNative + )]) + ); + } + make_table_from_iter(framework_addr, natives) } diff --git a/crates/aptos/src/governance/mod.rs b/crates/aptos/src/governance/mod.rs index 883e06fa9f2c1..092c88c6ae0e0 100644 --- a/crates/aptos/src/governance/mod.rs +++ b/crates/aptos/src/governance/mod.rs @@ -769,7 +769,7 @@ impl std::fmt::Display for ProposalMetadata { } } -fn compile_in_temp_dir( +pub fn compile_in_temp_dir( script_name: &str, script_path: &Path, framework_package_args: &FrameworkPackageArgs,