From 55a9772fcde7fe0f3ed3ca0622f8563be1df723a Mon Sep 17 00:00:00 2001 From: George Mitenkov Date: Tue, 7 Jul 2026 12:02:50 +0100 Subject: [PATCH] [WIP] Deferred module publish --- Cargo.lock | 2 + aptos-move/aptos-gas-meter/src/meter.rs | 9 + .../aptos-gas-profiling/src/profiler.rs | 6 + .../aptos-memory-usage-tracker/src/lib.rs | 2 + .../aptos-native-interface/src/context.rs | 13 +- aptos-move/aptos-vm/src/aptos_vm.rs | 201 +++++++------- aptos-move/aptos-vm/src/lib.rs | 7 + aptos-move/aptos-vm/src/verifier/mod.rs | 3 - .../src/code_cache_global_manager.rs | 13 + aptos-move/block-executor/src/executor.rs | 181 +++++++----- .../src/txn_last_input_output.rs | 18 ++ aptos-move/e2e-move-test-harness/src/lib.rs | 46 +++- .../e2e-move-tests/src/tests/chain_id.rs | 11 +- .../src/tests/code_publishing.rs | 25 +- aptos-move/e2e-move-tests/src/tests/events.rs | 8 +- .../e2e-move-tests/src/tests/init_module.rs | 13 +- aptos-move/e2e-tests/src/executor.rs | 116 ++++++-- .../aptos-framework/sources/block.move | 9 +- .../aptos-framework/sources/code.move | 108 +++++++- .../aptos-framework/sources/genesis.move | 2 + .../framework/cached-packages/src/head.mrb | Bin 1382343 -> 1384670 bytes .../move-stdlib/sources/configs/features.move | 28 ++ aptos-move/framework/natives/Cargo.toml | 1 + aptos-move/framework/natives/src/code.rs | 257 +++++++++++++++++- aptos-move/framework/natives/src/lib.rs | 1 + .../publish_verification}/event_validation.rs | 10 +- .../natives/src/publish_verification/mod.rs | 11 + .../native_validation.rs | 2 +- .../src/publish_verification/publish.rs | 108 ++++++++ .../publish_verification}/resource_groups.rs | 12 +- consensus/src/pipeline/pipeline_builder.rs | 8 + execution/executor-types/Cargo.toml | 1 + .../executor-types/src/execution_output.rs | 10 + execution/executor-types/src/lib.rs | 8 + execution/executor/src/block_executor/mod.rs | 23 ++ .../src/workflow/do_get_execution_output.rs | 11 + .../move-vm/runtime/src/native_functions.rs | 32 ++- third_party/move/move-vm/types/src/gas.rs | 8 + types/src/on_chain_config/aptos_features.rs | 10 + types/src/transaction/block_output.rs | 15 + 40 files changed, 1103 insertions(+), 246 deletions(-) rename aptos-move/{aptos-vm/src/verifier => framework/natives/src/publish_verification}/event_validation.rs (97%) create mode 100644 aptos-move/framework/natives/src/publish_verification/mod.rs rename aptos-move/{aptos-vm/src/verifier => framework/natives/src/publish_verification}/native_validation.rs (92%) create mode 100644 aptos-move/framework/natives/src/publish_verification/publish.rs rename aptos-move/{aptos-vm/src/verifier => framework/natives/src/publish_verification}/resource_groups.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 97b0fe99a1c..7294461424a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1726,6 +1726,7 @@ dependencies = [ "criterion", "derive_more 0.99.17", "itertools 0.13.0", + "move-core-types", "once_cell", "ouroboros", "rayon", @@ -2071,6 +2072,7 @@ dependencies = [ "blake2-rfc", "bulletproofs", "byteorder", + "bytes", "claims", "curve25519-dalek-ng", "either", diff --git a/aptos-move/aptos-gas-meter/src/meter.rs b/aptos-move/aptos-gas-meter/src/meter.rs index ddbb37f20f3..7f452a87afc 100644 --- a/aptos-move/aptos-gas-meter/src/meter.rs +++ b/aptos-move/aptos-gas-meter/src/meter.rs @@ -92,6 +92,15 @@ where fn use_heap_memory_in_native_context(&mut self, _amount: u64) -> PartialVMResult<()> { Ok(()) } + + fn charge_io_gas_for_native_write(&mut self, num_bytes: NumBytes) -> PartialVMResult<()> { + // A single new state slot whose key and value together occupy `num_bytes` bytes. This + // mirrors the per-write I/O in `io_gas_per_write` for gas feature version >= 12 (the only + // versions where deferred module publishing can be enabled). + let cost = STORAGE_IO_PER_STATE_SLOT_WRITE * NumArgs::new(1) + + STORAGE_IO_PER_STATE_BYTE_WRITE * num_bytes; + self.algebra.charge_io(cost) + } } impl GasMeter for StandardGasMeter diff --git a/aptos-move/aptos-gas-profiling/src/profiler.rs b/aptos-move/aptos-gas-profiling/src/profiler.rs index 592aa7b7286..bce6065172b 100644 --- a/aptos-move/aptos-gas-profiling/src/profiler.rs +++ b/aptos-move/aptos-gas-profiling/src/profiler.rs @@ -210,6 +210,12 @@ where res } + fn charge_io_gas_for_native_write(&mut self, num_bytes: NumBytes) -> PartialVMResult<()> { + let (_cost, res) = + self.delegate_charge(|base| base.charge_io_gas_for_native_write(num_bytes)); + res + } + fn charge_native_execution(&mut self, amount: InternalGas) -> PartialVMResult<()> { // Delegate first so we can record the actual amount charged -- this matters // when the base meter only partially charges (e.g. hitting OUT_OF_GAS). diff --git a/aptos-move/aptos-memory-usage-tracker/src/lib.rs b/aptos-move/aptos-memory-usage-tracker/src/lib.rs index c829292c90c..4f3f9d3f1f1 100644 --- a/aptos-move/aptos-memory-usage-tracker/src/lib.rs +++ b/aptos-move/aptos-memory-usage-tracker/src/lib.rs @@ -214,6 +214,8 @@ where delegate_mut! { fn charge_native_execution(&mut self, amount: InternalGas) -> PartialVMResult<()>; + + fn charge_io_gas_for_native_write(&mut self, num_bytes: NumBytes) -> PartialVMResult<()>; } #[inline] diff --git a/aptos-move/aptos-native-interface/src/context.rs b/aptos-move/aptos-native-interface/src/context.rs index fa45eb4d437..b4d5c80a955 100644 --- a/aptos-move/aptos-native-interface/src/context.rs +++ b/aptos-move/aptos-native-interface/src/context.rs @@ -12,7 +12,9 @@ use aptos_gas_schedule::{ use aptos_types::on_chain_config::{Features, TimedFeatureFlag, TimedFeatures}; use move_binary_format::errors::{Location, PartialVMResult, VMResult}; use move_core_types::{ - gas_algebra::InternalGas, identifier::Identifier, language_storage::ModuleId, + gas_algebra::{InternalGas, NumBytes}, + identifier::Identifier, + language_storage::ModuleId, }; use move_vm_runtime::{ native_extensions::NativeContextExtensions, @@ -117,6 +119,15 @@ impl<'b, 'c> SafeNativeContext<'_, 'b, 'c, '_> { .map_err(LimitExceededError::from_err) } + /// Charges I/O gas for a future write of a state slot whose key and value together occupy + /// `num_bytes` bytes. Used by `code::verify_package` to pre-charge the module-write I/O that is + /// materialized at the block epilogue. + pub fn charge_io_gas_for_write(&mut self, num_bytes: NumBytes) -> SafeNativeResult<()> { + self.gas_meter() + .charge_io_gas_for_native_write(num_bytes) + .map_err(LimitExceededError::from_err) + } + /// Evaluates the given gas expression within the current context immediately. /// /// This can be useful if you have branch conditions depending on gas parameters. diff --git a/aptos-move/aptos-vm/src/aptos_vm.rs b/aptos-move/aptos-vm/src/aptos_vm.rs index 7cbf2fba350..c673f53e86b 100644 --- a/aptos-move/aptos-vm/src/aptos_vm.rs +++ b/aptos-move/aptos-vm/src/aptos_vm.rs @@ -19,6 +19,7 @@ use crate::{ }, view_with_change_set::ExecutorViewWithChangeSet, }, + write_op_converter::WriteOpConverter, AptosMoveResolver, AsExecutorView, AsResourceGroupView, SessionExt, SessionId, UserTransactionContext, }, @@ -26,10 +27,7 @@ use crate::{ system_module_names::*, transaction_metadata::TransactionMetadata, transaction_validation, - verifier::{ - event_validation, native_validation, resource_groups, transaction_arg_validation, - view_function, - }, + verifier::{transaction_arg_validation, view_function}, VMBlockExecutor, VMValidator, }; use aptos_block_executor::{ @@ -85,7 +83,6 @@ use aptos_types::{ }, vm::module_metadata::{ get_compilation_metadata, get_metadata, get_randomness_annotation_for_entry_function, - verify_module_metadata_for_module_publishing, }, vm_status::{AbortLocation, StatusCode, VMStatus}, write_set::WriteOp, @@ -112,6 +109,7 @@ use aptos_vm_types::{ }, storage::{change_set_configs::ChangeSetConfigs, StorageGasParameters}, }; +use bytes::Bytes; use claims::assert_err; use fail::fail_point; use move_binary_format::{ @@ -984,7 +982,7 @@ impl AptosVM { // Check that unstable bytecode cannot be executed on mainnet and verify events. let script = func.owner_as_script()?; self.reject_unstable_bytecode_for_script(script)?; - event_validation::verify_no_event_emission_in_compiled_script(script)?; + aptos_framework_natives::publish_verification::event_validation::verify_no_event_emission_in_compiled_script(script)?; // Record the script's declared module dependencies as reads. These are a function of // the script bytecode, so recording them here keeps the read set independent of the @@ -1808,77 +1806,19 @@ impl AptosVM { traversal_context: &mut TraversalContext, gas_meter: &mut impl GasMeter, modules: &[CompiledModule], - mut expected_modules: BTreeSet, + expected_modules: BTreeSet, allowed_deps: Option>>, ) -> VMResult<()> { - self.reject_unstable_bytecode(modules)?; - native_validation::validate_module_natives(modules)?; - - for m in modules { - if !expected_modules.remove(m.self_id().name().as_str()) { - return Err(Self::metadata_validation_error(&format!( - "unregistered module: '{}'", - m.self_id().name() - ))); - } - if let Some(allowed) = &allowed_deps { - for dep in m.immediate_dependencies() { - if !allowed - .get(dep.address()) - .map(|modules| { - modules.contains("") || modules.contains(dep.name().as_str()) - }) - .unwrap_or(false) - { - return Err(Self::metadata_validation_error(&format!( - "unregistered dependency: '{}'", - dep - ))); - } - } - } - verify_module_metadata_for_module_publishing(m, self.features()) - .map_err(|err| Self::metadata_validation_error(&err.to_string()))?; - } - - resource_groups::validate_resource_groups( + aptos_framework_natives::publish_verification::publish::validate_publish_request( self.features(), + self.chain_id().is_mainnet(), module_storage, traversal_context, gas_meter, modules, - )?; - event_validation::validate_module_events( - self.features(), - module_storage, - traversal_context, - modules, - )?; - - if !expected_modules.is_empty() { - return Err(Self::metadata_validation_error( - "not all registered modules published", - )); - } - Ok(()) - } - - /// Check whether the bytecode can be published to mainnet based on the unstable tag in the metadata - fn reject_unstable_bytecode(&self, modules: &[CompiledModule]) -> VMResult<()> { - if self.chain_id().is_mainnet() { - for module in modules { - if let Some(metadata) = get_compilation_metadata(module) { - if metadata.unstable { - return Err(PartialVMError::new(StatusCode::UNSTABLE_BYTECODE_REJECTED) - .with_message( - "code marked unstable is not published on mainnet".to_string(), - ) - .finish(Location::Undefined)); - } - } - } - } - Ok(()) + expected_modules, + allowed_deps, + ) } /// Check whether the script can be run on mainnet based on the unstable tag in the metadata @@ -1895,12 +1835,6 @@ impl AptosVM { Ok(()) } - fn metadata_validation_error(msg: &str) -> VMError { - PartialVMError::new(StatusCode::CONSTRAINT_NOT_SATISFIED) - .with_message(format!("metadata and code bundle mismatch: {}", msg)) - .finish(Location::Undefined) - } - fn validate_signed_transaction( &self, session: &mut SessionExt, @@ -2764,6 +2698,38 @@ impl AptosVM { Ok((VMStatus::Executed, output)) } + /// Materializes the packages returned by `block_epilogue` (each a vector of module bytecode + /// blobs) into a module write set. The bytecode was already verified by `code::verify_package` + /// when it was queued, so this only deserializes to recover the module ids and builds the + /// write ops (deciding creation vs. modification from the current on-chain state). + fn build_epilogue_module_write_set( + &self, + resolver: &impl AptosMoveResolver, + module_storage: &impl AptosModuleStorage, + packages: Vec>>, + ) -> Result { + let mut bundle = vec![]; + for package in packages { + for blob in package { + let module = + CompiledModule::deserialize_with_config(&blob, self.deserializer_config()) + .map_err(|_| { + PartialVMError::new(StatusCode::CODE_DESERIALIZATION_ERROR) + .finish(Location::Undefined) + .into_vm_status() + })?; + bundle.push((module.self_id(), Bytes::from(blob))); + } + } + + let woc = + WriteOpConverter::new(resolver, self.features().is_storage_slot_metadata_enabled()); + let write_ops = woc + .convert_modules_into_write_ops(module_storage, bundle.into_iter()) + .map_err(|e| e.finish(Location::Undefined).into_vm_status())?; + Ok(ModuleWriteSet::new(write_ops)) + } + fn process_block_epilogue( &self, resolver: &impl AptosMoveResolver, @@ -2810,30 +2776,63 @@ impl AptosVM { let traversal_storage = TraversalStorage::new(); let mut traversal_context = TraversalContext::new(&traversal_storage); - let output = match session - .execute_function_bypass_visibility( - &BLOCK_MODULE, - BLOCK_EPILOGUE, - vec![], - serialize_values(&args), - &mut gas_meter, - &mut traversal_context, - module_storage, - ) - .map(|_return_vals| ()) - .or_else(|e| expect_only_successful_execution(e, BLOCK_EPILOGUE.as_str(), log_context)) - { - Ok(_) => get_system_transaction_output( - session, - module_storage, - &self.storage_gas_params(log_context)?.change_set_configs, - )?, + let change_set_configs = &self.storage_gas_params(log_context)?.change_set_configs; + let output = match session.execute_function_bypass_visibility( + &BLOCK_MODULE, + BLOCK_EPILOGUE, + vec![], + serialize_values(&args), + &mut gas_meter, + &mut traversal_context, + module_storage, + ) { + Ok(return_vals) => { + // When deferred module publishing is enabled, `block_epilogue` returns the packages + // queued during the block. Materialize them into module writes on the epilogue + // output. When disabled, the returned vector is empty and is ignored. + let module_write_set = if self.features().is_deferred_module_publishing_enabled() { + let bytes = return_vals + .return_values + .into_iter() + .next() + .map(|(bytes, _layout)| bytes) + .ok_or_else(|| { + VMStatus::error( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + Some("block_epilogue must return the queued packages".to_string()), + ) + })?; + let packages: Vec>> = bcs::from_bytes(&bytes).map_err(|_| { + VMStatus::error( + StatusCode::UNKNOWN_INVARIANT_VIOLATION_ERROR, + Some("failed to deserialize queued packages".to_string()), + ) + })?; + self.build_epilogue_module_write_set(resolver, module_storage, packages)? + } else { + ModuleWriteSet::empty() + }; + let change_set = session.finish(change_set_configs, module_storage)?; + VMOutput::new( + change_set, + module_write_set, + FeeStatement::zero(), + TransactionStatus::Keep(ExecutionStatus::Success), + ) + }, Err(e) => { - error!( - "Unexpected error from BlockEpilogue txn: {e:?}, fallback to return success." - ); - let status = TransactionStatus::Keep(ExecutionStatus::Success); - VMOutput::empty_with_status(status) + match expect_only_successful_execution(e, BLOCK_EPILOGUE.as_str(), log_context) { + Ok(_) => { + get_system_transaction_output(session, module_storage, change_set_configs)? + }, + Err(e) => { + error!( + "Unexpected error from BlockEpilogue txn: {e:?}, fallback to return success." + ); + let status = TransactionStatus::Keep(ExecutionStatus::Success); + VMOutput::empty_with_status(status) + }, + } }, }; @@ -3407,6 +3406,10 @@ impl VMBlockExecutor for AptosVMBlockExecutor { } } + fn invalidate_published_modules(&self, module_ids: Vec) { + self.module_cache_manager.invalidate(module_ids); + } + fn execute_block( &self, txn_provider: &DefaultTxnProvider, diff --git a/aptos-move/aptos-vm/src/lib.rs b/aptos-move/aptos-vm/src/lib.rs index 50d6d7457de..7e0c5d6b33a 100644 --- a/aptos-move/aptos-vm/src/lib.rs +++ b/aptos-move/aptos-vm/src/lib.rs @@ -139,6 +139,7 @@ use aptos_types::{ }, vm_status::VMStatus, }; +use move_core_types::language_storage::ModuleId; use move_vm_runtime::ModuleStorage; use std::{marker::Sync, sync::Arc}; pub use verifier::view_function::determine_is_view; @@ -175,6 +176,12 @@ pub trait VMBlockExecutor: Send + Sync { transaction_slice_metadata: TransactionSliceMetadata, ) -> Result, VMStatus>; + /// Invalidates the given modules in the executor's cross-block module cache. Called at commit + /// time for a block that published modules via the deferred publishing flow, so the next block + /// re-reads the new code from storage. Default is a no-op for executors without a persistent + /// module cache. + fn invalidate_published_modules(&self, _module_ids: Vec) {} + /// Executes a block of transactions and returns output for each one of them, without applying /// any block limit. fn execute_block_no_limit( diff --git a/aptos-move/aptos-vm/src/verifier/mod.rs b/aptos-move/aptos-vm/src/verifier/mod.rs index 9cd03dcc770..6e47b52d23b 100644 --- a/aptos-move/aptos-vm/src/verifier/mod.rs +++ b/aptos-move/aptos-vm/src/verifier/mod.rs @@ -1,8 +1,5 @@ // Copyright (c) Aptos Foundation // Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE -pub(crate) mod event_validation; pub(crate) mod module_init; -pub(crate) mod native_validation; -pub(crate) mod resource_groups; pub mod transaction_arg_validation; pub(crate) mod view_function; diff --git a/aptos-move/block-executor/src/code_cache_global_manager.rs b/aptos-move/block-executor/src/code_cache_global_manager.rs index 3afe09ca7b5..5801fe1a82a 100644 --- a/aptos-move/block-executor/src/code_cache_global_manager.rs +++ b/aptos-move/block-executor/src/code_cache_global_manager.rs @@ -243,6 +243,19 @@ impl AptosModuleCacheManager { Ok(guard) } + + /// Marks the given modules as overridden in the global module cache so the next block re-reads + /// them from storage. Called at commit time for a block that published modules via the deferred + /// publishing flow. Best-effort: if the manager lock cannot be acquired the invalidation is + /// skipped (the publishing barrier ensures the next block does not execute against a stale cache + /// before this runs). + pub fn invalidate(&self, module_ids: impl IntoIterator) { + if let Some(guard) = self.inner.try_lock() { + for module_id in module_ids { + guard.module_cache.mark_overridden(&module_id); + } + } + } } /// A guard that can be acquired from [AptosModuleCacheManager]. Variants represent successful and diff --git a/aptos-move/block-executor/src/executor.rs b/aptos-move/block-executor/src/executor.rs index 430ee52b541..cb9fecdf860 100644 --- a/aptos-move/block-executor/src/executor.rs +++ b/aptos-move/block-executor/src/executor.rs @@ -1636,9 +1636,12 @@ where scheduler: SchedulerWrapper, environment: &AptosEnvironment, shared_sync_params: &SharedSyncParams, - ) -> Result, PanicError> { + ) -> Result<(Option, Vec), PanicError> { let _timer = PARALLEL_FINALIZE_SECONDS.start_timer(); let mut maybe_block_epilogue_txn = None; + // Modules published by the block epilogue (deferred publishing). Recorded so the global + // module cache can be invalidated at commit time. + let mut published_module_ids = vec![]; let versioned_cache = shared_sync_params.versioned_cache; let num_txns = signature_verified_block.num_txns(); @@ -1753,6 +1756,10 @@ where runtime_environment, &self.config, )?; + // Record the modules published by the epilogue before its output is materialized. + // The epilogue bypasses `publish_module_write_set`, so its module writes are not + // added to the module cache during execution; they are invalidated at commit time. + published_module_ids = last_input_output.published_module_ids(epilogue_txn_idx)?; self.materialize_txn_commit( epilogue_txn_idx, scheduler, @@ -1773,7 +1780,7 @@ where final_results.acquire().dereference_mut().pop(); } - Ok(maybe_block_epilogue_txn) + Ok((maybe_block_epilogue_txn, published_module_ids)) } pub(crate) fn execute_transactions_parallel_v2( @@ -1898,32 +1905,34 @@ where }); drop(timer); - let (has_error, maybe_block_epilogue_txn) = if shared_maybe_error.load(Ordering::SeqCst) { - (true, None) - } else { - match self.finalize_parallel_execution( - maybe_executor.into_inner(), - signature_verified_block, - !scheduler.post_commit_processing_queue_is_empty(), - transaction_slice_metadata, - SchedulerWrapper::V2(&scheduler, 0), - module_cache_manager_guard.environment(), - &shared_sync_params, - ) { - Ok(maybe_block_epilogue_txn) => { - // Update state counters & insert verified modules into cache (safe after error check). - counters::update_state_counters(versioned_cache.stats(), true); - ( - module_cache_manager_guard - .module_cache_mut() - .insert_verified(versioned_cache.take_modules_iter()) - .is_err(), - maybe_block_epilogue_txn, - ) - }, - Err(_) => (true, None), - } - }; + let (has_error, maybe_block_epilogue_txn, published_module_ids) = + if shared_maybe_error.load(Ordering::SeqCst) { + (true, None, vec![]) + } else { + match self.finalize_parallel_execution( + maybe_executor.into_inner(), + signature_verified_block, + !scheduler.post_commit_processing_queue_is_empty(), + transaction_slice_metadata, + SchedulerWrapper::V2(&scheduler, 0), + module_cache_manager_guard.environment(), + &shared_sync_params, + ) { + Ok((maybe_block_epilogue_txn, published_module_ids)) => { + // Update state counters & insert verified modules into cache (safe after error check). + counters::update_state_counters(versioned_cache.stats(), true); + ( + module_cache_manager_guard + .module_cache_mut() + .insert_verified(versioned_cache.take_modules_iter()) + .is_err(), + maybe_block_epilogue_txn, + published_module_ids, + ) + }, + Err(_) => (true, None, vec![]), + } + }; // Explicit async drops even when there is an error. DEFAULT_DROPPER.schedule_drop((last_input_output, scheduler, versioned_cache)); @@ -1933,10 +1942,10 @@ where } // Return final result - Ok(BlockOutput::new( - final_results.into_inner(), - maybe_block_epilogue_txn, - )) + Ok( + BlockOutput::new(final_results.into_inner(), maybe_block_epilogue_txn) + .with_published_modules(published_module_ids), + ) } /// Testing-only public wrapper (behind the `testing` feature) so PoCs in other crates can @@ -2082,32 +2091,34 @@ where }); drop(timer); - let (has_error, maybe_block_epilogue_txn) = if shared_maybe_error.load(Ordering::SeqCst) { - (true, None) - } else { - match self.finalize_parallel_execution( - maybe_executor.into_inner(), - signature_verified_block, - scheduler.pop_from_commit_queue().is_ok(), - transaction_slice_metadata, - SchedulerWrapper::V1(&scheduler, &skip_module_reads_validation), - module_cache_manager_guard.environment(), - &shared_sync_params, - ) { - Ok(maybe_block_epilogue_txn) => { - // Update state counters & insert verified modules into cache (safe after error check). - counters::update_state_counters(versioned_cache.stats(), true); - ( - module_cache_manager_guard - .module_cache_mut() - .insert_verified(versioned_cache.take_modules_iter()) - .is_err(), - maybe_block_epilogue_txn, - ) - }, - Err(_) => (true, None), - } - }; + let (has_error, maybe_block_epilogue_txn, published_module_ids) = + if shared_maybe_error.load(Ordering::SeqCst) { + (true, None, vec![]) + } else { + match self.finalize_parallel_execution( + maybe_executor.into_inner(), + signature_verified_block, + scheduler.pop_from_commit_queue().is_ok(), + transaction_slice_metadata, + SchedulerWrapper::V1(&scheduler, &skip_module_reads_validation), + module_cache_manager_guard.environment(), + &shared_sync_params, + ) { + Ok((maybe_block_epilogue_txn, published_module_ids)) => { + // Update state counters & insert verified modules into cache (safe after error check). + counters::update_state_counters(versioned_cache.stats(), true); + ( + module_cache_manager_guard + .module_cache_mut() + .insert_verified(versioned_cache.take_modules_iter()) + .is_err(), + maybe_block_epilogue_txn, + published_module_ids, + ) + }, + Err(_) => (true, None, vec![]), + } + }; // Explicit async drops even when there is an error. DEFAULT_DROPPER.schedule_drop((last_input_output, scheduler, versioned_cache)); @@ -2117,10 +2128,10 @@ where } // Return final result - Ok(BlockOutput::new( - final_results.into_inner(), - maybe_block_epilogue_txn, - )) + Ok( + BlockOutput::new(final_results.into_inner(), maybe_block_epilogue_txn) + .with_published_modules(published_module_ids), + ) } fn gen_block_epilogue<'a>( @@ -2141,7 +2152,12 @@ where { return Ok(T::state_checkpoint(block_id)); } - if !features.is_calculate_transaction_fee_for_distribution_enabled() { + // A V0 payload has no Move call, so the code publish queue would not be drained. When + // deferred module publishing is enabled, produce a Move-calling payload (with an empty fee + // distribution) so the epilogue runs every block and drains the queue. + if !features.is_calculate_transaction_fee_for_distribution_enabled() + && !features.is_deferred_module_publishing_enabled() + { return Ok(T::block_epilogue_v0( block_id, block_end_info.to_persistent(), @@ -2230,6 +2246,7 @@ where T::Key, (TriompheArc, Option>), >, + is_block_epilogue: bool, ) -> Result<(), SequentialBlockExecutionError> { for (key, (write_op, layout)) in resource_write_set.into_iter() { unsync_map.write(key, write_op, layout); @@ -2246,14 +2263,19 @@ where unsync_map.write(key, TriompheArc::new(write_op), None); } - for write in output_before_guard.module_write_set().values() { - add_module_write_to_module_cache::( - write, - txn_idx, - runtime_environment, - global_module_cache, - unsync_map.module_cache(), - )?; + // The block epilogue's module writes (deferred publishing) are not added to the module + // cache during execution; they are invalidated at commit time instead. For all other + // transactions, publish module writes into the per-block and global caches as usual. + if !is_block_epilogue { + for write in output_before_guard.module_write_set().values() { + add_module_write_to_module_cache::( + write, + txn_idx, + runtime_environment, + global_module_cache, + unsync_map.module_cache(), + )?; + } } let mut second_phase = Vec::new(); @@ -2339,6 +2361,9 @@ where ); let mut block_epilogue_txn = None; + // Modules published by the block epilogue (deferred publishing), recorded so the global + // module cache can be invalidated at commit time. + let mut published_module_ids = vec![]; // Counts user-txn `accumulate_fee_statement` calls. Incremented alongside each // accumulate so any loop-exit path (including the bcs-fallback `continue`) keeps // this in sync. Passed as `num_committed` to `finish_*` so block-level counters @@ -2556,6 +2581,17 @@ where ); } + // Record the modules published by the block epilogue (deferred publishing) so + // the global module cache can be invalidated at commit time. + let is_block_epilogue = idx == num_txns; + if is_block_epilogue { + published_module_ids = output_before_guard + .module_write_set() + .values() + .map(|write| write.module_id().clone()) + .collect(); + } + // Apply the writes. let resource_write_set = output_before_guard.resource_write_set(); Self::apply_output_sequential( @@ -2565,6 +2601,7 @@ where &unsync_map, &output_before_guard, resource_write_set.clone(), + is_block_epilogue, )?; // If dynamic change set materialization part (indented for clarity/variable scope): @@ -2690,7 +2727,7 @@ where .module_cache_mut() .insert_verified(unsync_map.into_modules_iter())?; - Ok(BlockOutput::new(ret, block_epilogue_txn)) + Ok(BlockOutput::new(ret, block_epilogue_txn).with_published_modules(published_module_ids)) } pub fn execute_block( diff --git a/aptos-move/block-executor/src/txn_last_input_output.rs b/aptos-move/block-executor/src/txn_last_input_output.rs index dd2a320482e..86b368fc277 100644 --- a/aptos-move/block-executor/src/txn_last_input_output.rs +++ b/aptos-move/block-executor/src/txn_last_input_output.rs @@ -575,6 +575,24 @@ impl> TxnLastInputOutput { Ok(published) } + /// Returns the ids of the modules written by the transaction (empty if none). Used to record + /// the modules published by the block epilogue so the global module cache can be invalidated + /// at commit time. Must be called before the output is materialized. + pub(crate) fn published_module_ids( + &self, + txn_idx: TxnIndex, + ) -> Result, PanicError> { + let output_wrapper = self.output_wrappers[txn_idx as usize].lock(); + let output_before_guard = output_wrapper + .check_success_or_skip_status()? + .before_materialization()?; + Ok(output_before_guard + .module_write_set() + .values() + .map(|write| write.module_id().clone()) + .collect()) + } + pub(crate) fn delayed_field_keys( &self, txn_idx: TxnIndex, diff --git a/aptos-move/e2e-move-test-harness/src/lib.rs b/aptos-move/e2e-move-test-harness/src/lib.rs index 666a5d8b2cf..7edffa729dd 100644 --- a/aptos-move/e2e-move-test-harness/src/lib.rs +++ b/aptos-move/e2e-move-test-harness/src/lib.rs @@ -275,13 +275,21 @@ impl MoveHarnessImpl { self.new_account_at(AccountAddress::ONE) } - /// Runs a signed transaction. On success, applies the write set. + /// Runs a signed transaction. On success, applies the write set. When the block epilogue is + /// enabled (deferred module publishing), the transaction is run as a one-transaction block and + /// the block epilogue's write set (which materializes published modules) is also applied. pub fn run_raw(&mut self, txn: SignedTransaction) -> TransactionOutput { - let output = self.executor.execute_transaction(txn); + let mut outputs = self.executor.execute_block(vec![txn]).unwrap().into_iter(); + let output = outputs.next().expect("at least one output"); if matches!(output.status(), TransactionStatus::Keep(_)) { self.executor.apply_write_set(output.write_set()); self.executor.append_events(output.events().to_vec()); } + for epilogue_output in outputs { + if matches!(epilogue_output.status(), TransactionStatus::Keep(_)) { + self.executor.apply_write_set(epilogue_output.write_set()); + } + } output } @@ -295,21 +303,39 @@ impl MoveHarnessImpl { &mut self, txn: SignedTransaction, ) -> (TransactionStatus, Vec) { - let output = self.executor.execute_transaction(txn); + let mut outputs = self.executor.execute_block(vec![txn]).unwrap().into_iter(); + let output = outputs.next().expect("at least one output"); if matches!(output.status(), TransactionStatus::Keep(_)) { self.executor.apply_write_set(output.write_set()); } + // Apply the block epilogue's write set (materializes deferred module publishes). + for epilogue_output in outputs { + if matches!(epilogue_output.status(), TransactionStatus::Keep(_)) { + self.executor.apply_write_set(epilogue_output.write_set()); + } + } (output.status().to_owned(), output.events().to_owned()) } - /// Runs a block of signed transactions. On success, applies the write set. + /// Runs a block of signed transactions. On success, applies the write set. When a block + /// epilogue is appended (deferred module publishing), its write set is applied but its status + /// is not returned, so callers observe exactly one status per input transaction. pub fn run_block(&mut self, txn_block: Vec) -> Vec { + let num_txns = txn_block.len(); let mut result = vec![]; - for output in self.executor.execute_block(txn_block).unwrap() { + for (i, output) in self + .executor + .execute_block(txn_block) + .unwrap() + .into_iter() + .enumerate() + { if matches!(output.status(), TransactionStatus::Keep(_)) { self.executor.apply_write_set(output.write_set()); } - result.push(output.status().to_owned()) + if i < num_txns { + result.push(output.status().to_owned()); + } } result } @@ -332,17 +358,21 @@ impl MoveHarnessImpl { result } - /// Runs a block of signed transactions. On success, applies the write set. + /// Runs a block of signed transactions. On success, applies the write set. When a block + /// epilogue is appended (deferred module publishing), its write set is applied but its output + /// is not returned, so callers observe exactly one output per input transaction. pub fn run_block_get_output( &mut self, txn_block: Vec, ) -> Vec { - let result = assert_ok!(self.executor.execute_block(txn_block)); + let num_txns = txn_block.len(); + let mut result = assert_ok!(self.executor.execute_block(txn_block)); for output in &result { if matches!(output.status(), TransactionStatus::Keep(_)) { self.executor.apply_write_set(output.write_set()); } } + result.truncate(num_txns); result } diff --git a/aptos-move/e2e-move-tests/src/tests/chain_id.rs b/aptos-move/e2e-move-tests/src/tests/chain_id.rs index b74224e6aed..08894a7c657 100644 --- a/aptos-move/e2e-move-tests/src/tests/chain_id.rs +++ b/aptos-move/e2e-move-tests/src/tests/chain_id.rs @@ -3,6 +3,7 @@ use crate::{assert_success, tests::common, MoveHarness}; use aptos_language_e2e_tests::account::Account; +use aptos_types::on_chain_config::FeatureFlag; use move_core_types::{account_address::AccountAddress, parser::parse_struct_tag}; use serde::{Deserialize, Serialize}; @@ -63,7 +64,10 @@ fn setup(harness: &mut MoveHarness) -> Account { #[test] fn test_chain_id_from_aptos_framework() { - let mut harness = MoveHarness::new(); + // Deferred module publishing does not run init_module, which this package relies on to create + // the ChainIdStore resource, so keep it disabled here. + let mut harness = + MoveHarness::new_with_features(vec![], vec![FeatureFlag::DEFERRED_MODULE_PUBLISHING]); let account = setup(&mut harness); assert_eq!( @@ -74,7 +78,10 @@ fn test_chain_id_from_aptos_framework() { #[test] fn test_chain_id_from_type_info() { - let mut harness = MoveHarness::new(); + // Deferred module publishing does not run init_module, which this package relies on to create + // the ChainIdStore resource, so keep it disabled here. + let mut harness = + MoveHarness::new_with_features(vec![], vec![FeatureFlag::DEFERRED_MODULE_PUBLISHING]); let account = setup(&mut harness); assert_eq!( diff --git a/aptos-move/e2e-move-tests/src/tests/code_publishing.rs b/aptos-move/e2e-move-tests/src/tests/code_publishing.rs index 7244b23d7f1..084882aa2de 100644 --- a/aptos-move/e2e-move-tests/src/tests/code_publishing.rs +++ b/aptos-move/e2e-move-tests/src/tests/code_publishing.rs @@ -221,7 +221,12 @@ fn code_publishing_upgrade_fail_overlapping_module() { /// TODO: for some reason this test did not capture a serious bug in `code::check_coexistence`. #[test] fn code_publishing_upgrade_loader_cache_consistency() { - let mut h = MoveHarness::new(); + // This test publishes multiple packages to the same address in a single block, which the + // deferred module publishing flow does not allow (one publish per address per block). Run with + // the legacy publish flow. + let mut h = MoveHarness::new_with_features(vec![], vec![ + FeatureFlag::DEFERRED_MODULE_PUBLISHING, + ]); let acc = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); // Create a sequence of package upgrades @@ -322,7 +327,11 @@ fn code_publishing_using_resource_account() { #[test] fn code_publishing_with_two_attempts_and_verify_loader_is_invalidated() { - let mut h = MoveHarness::new(); + // This test relies on init_module running (and failing) during publish, which the deferred + // module publishing flow does not run. Run with the legacy publish flow. + let mut h = MoveHarness::new_with_features(vec![], vec![ + FeatureFlag::DEFERRED_MODULE_PUBLISHING, + ]); let acc = h.new_account_at(AccountAddress::from_hex_literal("0xcafe").unwrap()); // First module publish attempt failed when executing the init_module. @@ -429,6 +438,9 @@ fn test_module_publishing_does_not_fallback() { executor.disable_block_executor_fallback(); let mut h = MoveHarness::new_with_executor(executor); + // This test exercises mid-block module materialization and init_module, which are legacy + // (non-deferred) publish behaviors. Run with the legacy publish flow. + h.enable_features(vec![], vec![FeatureFlag::DEFERRED_MODULE_PUBLISHING]); let addr = AccountAddress::from_hex_literal("0x123").unwrap(); let account = h.new_account_at(addr); @@ -531,6 +543,9 @@ fn test_module_publishing_does_not_leak_speculative_information() { executor.disable_block_executor_fallback(); let mut h = MoveHarness::new_with_executor(executor); + // This test exercises mid-block speculative execution of module publishing, a legacy + // (non-deferred) publish behavior. Run with the legacy publish flow. + h.enable_features(vec![], vec![FeatureFlag::DEFERRED_MODULE_PUBLISHING]); let addr = AccountAddress::random(); let account = h.new_account_at(addr); @@ -604,7 +619,11 @@ fn assert_move_abort(status: TransactionStatus, expected_abort_code: u64) { #[test] fn test_init_module_should_not_publish_modules() { - let mut h = MoveHarness::new(); + // This test relies on init_module running during publish, which the deferred module publishing + // flow does not run. Run with the legacy publish flow. + let mut h = MoveHarness::new_with_features(vec![], vec![ + FeatureFlag::DEFERRED_MODULE_PUBLISHING, + ]); let addr = AccountAddress::from_hex_literal("0xcafe").unwrap(); let account = h.new_account_at(addr); diff --git a/aptos-move/e2e-move-tests/src/tests/events.rs b/aptos-move/e2e-move-tests/src/tests/events.rs index 072e4886860..e88f7fd9af0 100644 --- a/aptos-move/e2e-move-tests/src/tests/events.rs +++ b/aptos-move/e2e-move-tests/src/tests/events.rs @@ -3,14 +3,18 @@ use crate::{assert_success, tests::common, MoveHarness}; use aptos_framework::natives::event::ECANNOT_CREATE_EVENT; -use aptos_types::{move_utils::MemberId, transaction::ExecutionStatus}; +use aptos_types::{ + move_utils::MemberId, on_chain_config::FeatureFlag, transaction::ExecutionStatus, +}; use claims::assert_ok; use move_core_types::account_address::AccountAddress; use std::str::FromStr; #[test] fn test_events_ty_tag_size_too_large() { - let mut h = MoveHarness::new(); + // Deferred module publishing does not run init_module, which this package relies on to create + // the event-handle resources, so keep it disabled here. + let mut h = MoveHarness::new_with_features(vec![], vec![FeatureFlag::DEFERRED_MODULE_PUBLISHING]); let acc = h.new_account_at(AccountAddress::from_hex_literal("0x815").unwrap()); assert_success!(h.publish_package(&acc, &common::test_dir_path("events.data/pack"))); diff --git a/aptos-move/e2e-move-tests/src/tests/init_module.rs b/aptos-move/e2e-move-tests/src/tests/init_module.rs index b39e89ef449..d630099ce0d 100644 --- a/aptos-move/e2e-move-tests/src/tests/init_module.rs +++ b/aptos-move/e2e-move-tests/src/tests/init_module.rs @@ -6,6 +6,7 @@ use aptos_framework::{BuildOptions, BuiltPackage}; use aptos_package_builder::PackageBuilder; use aptos_types::{ account_address::AccountAddress, + on_chain_config::FeatureFlag, transaction::{ExecutionStatus, TransactionStatus}, }; use claims::assert_ok; @@ -21,7 +22,8 @@ struct ModuleData { #[test] fn init_module() { - let mut h = MoveHarness::new(); + // Deferred module publishing does not run init_module, so keep it disabled for these tests. + let mut h = MoveHarness::new_with_features(vec![], vec![FeatureFlag::DEFERRED_MODULE_PUBLISHING]); // Load the code let acc = h.aptos_framework_account(); @@ -49,7 +51,8 @@ fn init_module() { #[test] fn init_module_when_republishing_package() { - let mut h = MoveHarness::new(); + // Deferred module publishing does not run init_module, so keep it disabled for these tests. + let mut h = MoveHarness::new_with_features(vec![], vec![FeatureFlag::DEFERRED_MODULE_PUBLISHING]); // Deploy a package that initially does not have the module that has the init_module function. let acc = h.aptos_framework_account(); @@ -73,7 +76,8 @@ fn init_module_when_republishing_package() { #[test] fn init_module_with_abort_and_republish() { - let mut h = MoveHarness::new(); + // Deferred module publishing does not run init_module, so keep it disabled for these tests. + let mut h = MoveHarness::new_with_features(vec![], vec![FeatureFlag::DEFERRED_MODULE_PUBLISHING]); let acc = h.new_account_at(AccountAddress::from_hex_literal("0x12").unwrap()); let mut p1 = PackageBuilder::new("Pack"); @@ -104,7 +108,8 @@ fn init_module_with_abort_and_republish() { #[test_case(true)] #[test_case(false)] fn invalid_init_module(allow_extended_checks_to_fail: bool) { - let mut h = MoveHarness::new(); + // Deferred module publishing does not run init_module, so keep it disabled for these tests. + let mut h = MoveHarness::new_with_features(vec![], vec![FeatureFlag::DEFERRED_MODULE_PUBLISHING]); let acc = h.new_account_at(AccountAddress::from_hex_literal("0x42").unwrap()); let package_paths = [ diff --git a/aptos-move/e2e-tests/src/executor.rs b/aptos-move/e2e-tests/src/executor.rs index 2016a9e1440..c2e9e3f2803 100644 --- a/aptos-move/e2e-tests/src/executor.rs +++ b/aptos-move/e2e-tests/src/executor.rs @@ -54,7 +54,7 @@ use aptos_types::{ signature_verified_transaction::{ into_signature_verified_block, SignatureVerifiedTransaction, }, - AuxiliaryInfo, BlockOutput, ExecutionStatus, PersistedAuxiliaryInfo, SignedTransaction, + AuxiliaryInfo, ExecutionStatus, PersistedAuxiliaryInfo, SignedTransaction, Transaction, TransactionExecutableRef, TransactionOutput, TransactionStatus, VMValidatorResult, ViewFunctionOutput, }, @@ -147,6 +147,12 @@ fn empty_in_memory_state_store() -> FakeExecutorStateStore { enum BlockState { None, Fuzzing(SharedCacheState), + /// Executes each block with `TransactionSliceMetadata::Block`, so the block executor appends a + /// block epilogue. This is required to test the deferred module publishing flow (the publish + /// queue is drained and materialized by the epilogue). Uses a fresh module cache per block (like + /// `None`), so there is no cross-block cache staleness and sequential/parallel comparison runs + /// stay consistent. + BlockEpilogue { next_block_id: Cell }, } /// Shared cache state used only in fuzzing/test flows to enable hot module cache persistence and @@ -248,6 +254,19 @@ impl FakeExecutorImpl { block_state: BlockState::None, }; executor.apply_write_set(write_set); + // When deferred module publishing is enabled, modules are materialized by the block + // epilogue. Run each block with a block epilogue so that single-transaction test APIs (which + // execute a one-transaction block) still publish modules. Deferred publishing is coupled to + // lazy loading (the eager path keeps using the legacy publish flow), so both must be on. + if Features::fetch_config(&executor.state_store) + .ok() + .flatten() + .is_some_and(|features| { + features.is_deferred_module_publishing_enabled() && features.is_lazy_loading_enabled() + }) + { + executor.enable_block_epilogue(); + } executor } @@ -835,12 +854,12 @@ impl FakeExecutorImpl { /// 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(&mut self, transaction: SignedTransaction) -> TransactionOutput { - let mut outputs = self.execute_block(vec![transaction]).unwrap(); - assert!(outputs.len() == 1, "transaction outputs size mismatch"); - let output = outputs.pop().unwrap(); + let outputs = self.execute_block(vec![transaction]).unwrap(); + // The first output is the user transaction; in block-epilogue mode a block epilogue output + // follows (materializes deferred module publishes). Apply all `Keep` outputs. + let output = self.apply_block_outputs(outputs); match output.status() { TransactionStatus::Keep(status) => { - self.apply_write_set(output.write_set()); assert_eq!( status, &ExecutionStatus::Success, @@ -854,6 +873,25 @@ impl FakeExecutorImpl { } } + /// Applies the write sets of all `Keep` outputs (the user transaction and any block epilogue + /// appended in block-epilogue mode) to the data store, and returns the first (user transaction) + /// output. + fn apply_block_outputs(&mut self, outputs: Vec) -> TransactionOutput { + let mut outputs = outputs.into_iter(); + let user_output = outputs + .next() + .expect("A block must have at least one output"); + if matches!(user_output.status(), TransactionStatus::Keep(_)) { + self.apply_write_set(user_output.write_set()); + } + for epilogue_output in outputs { + if matches!(epilogue_output.status(), TransactionStatus::Keep(_)) { + self.apply_write_set(epilogue_output.write_set()); + } + } + user_output + } + fn execute_transaction_block_impl_with_state_view( &self, txn_block: Vec, @@ -872,7 +910,7 @@ impl FakeExecutorImpl { .collect::>(); let txn_provider = DefaultTxnProvider::new(txn_block, auxiliary_info); let metadata = self.get_txn_slice_metadata(); - let result = { + let block_output = AptosVMBlockExecutorWrapper::execute_block::<_, NoOpTransactionCommitHook, _>( &txn_provider, &state_view, @@ -881,11 +919,21 @@ impl FakeExecutorImpl { config, metadata, None, - ) - .map(BlockOutput::into_transaction_outputs_forced) - }; - let outputs = result?; - Ok(outputs) + )?; + + // Invalidate the modules published by the block epilogue (deferred module publishing) in + // the shared module cache, mirroring the commit-time invalidation done by the real block + // executor. Without this, a subsequent block would read stale cached code for an upgraded + // module. Only relevant when a shared cache manager is used (block mode); with a fresh + // per-call cache there is nothing to invalidate. + if let Some(manager) = self.module_cache_manager_opt() { + let published_modules = block_output.published_modules().to_vec(); + if !published_modules.is_empty() { + manager.invalidate(published_modules); + } + } + + Ok(block_output.into_transaction_outputs_forced()) } /// Returns a reference to the shared module cache manager if enabled (fuzzing/test). Otherwise @@ -893,24 +941,35 @@ impl FakeExecutorImpl { fn module_cache_manager_opt(&self) -> Option<&AptosModuleCacheManager> { match &self.block_state { BlockState::Fuzzing(shared) => Some(&shared.manager), - BlockState::None => None, + BlockState::None | BlockState::BlockEpilogue { .. } => None, } } /// Generates a [TransactionSliceMetadata::Block] when running with a shared cache (fuzzing/test) /// to enable cache reuse across calls. For normal runs, returns [None]. fn get_txn_slice_metadata(&self) -> TransactionSliceMetadata { - match &self.block_state { - BlockState::Fuzzing(shared) => { - let child = shared.next_block_id.get(); - shared.next_block_id.set(child + 1); - TransactionSliceMetadata::block( - HashValue::from_u64(child - 1), - HashValue::from_u64(child), - ) - }, - BlockState::None => TransactionSliceMetadata::unknown(), - } + let next_block_id = match &self.block_state { + BlockState::Fuzzing(shared) => &shared.next_block_id, + BlockState::BlockEpilogue { next_block_id } => next_block_id, + BlockState::None => return TransactionSliceMetadata::unknown(), + }; + let child = next_block_id.get(); + next_block_id.set(child + 1); + TransactionSliceMetadata::block( + HashValue::from_u64(child - 1), + HashValue::from_u64(child), + ) + } + + /// Enables block-epilogue mode: each executed block appends a block epilogue transaction. This + /// is required to exercise the deferred module publishing flow, where the publish queue is + /// drained and modules are materialized by the epilogue. Note that in this mode a block's + /// outputs include the epilogue output, so use the block APIs (e.g. `run_block`) rather than the + /// single-transaction ones. + pub fn enable_block_epilogue(&mut self) { + self.block_state = BlockState::BlockEpilogue { + next_block_id: Cell::new(1), + }; } #[cfg(fuzzing)] @@ -928,7 +987,7 @@ impl FakeExecutorImpl { .unwrap(); Some(guard.snapshot_hot_cache()) }, - BlockState::None => None, + BlockState::None | BlockState::BlockEpilogue { .. } => None, } } @@ -1118,12 +1177,15 @@ impl FakeExecutorImpl { pub fn execute_transaction(&self, txn: SignedTransaction) -> TransactionOutput { let txn_block = vec![txn]; - let mut outputs = self + let outputs = self .execute_block(txn_block) .expect("The VM should not fail to startup"); + // The first output is the user transaction; in block-epilogue mode a block epilogue output + // follows (discarded here, since this method does not apply write sets). outputs - .pop() - .expect("A block with one transaction should have one output") + .into_iter() + .next() + .expect("A block with one transaction should have at least one output") } pub fn execute_transaction_with_gas_profiler( diff --git a/aptos-move/framework/aptos-framework/sources/block.move b/aptos-move/framework/aptos-framework/sources/block.move index 91c8e4cb13a..b3f70484164 100644 --- a/aptos-move/framework/aptos-framework/sources/block.move +++ b/aptos-move/framework/aptos-framework/sources/block.move @@ -12,6 +12,7 @@ module aptos_framework::block { use aptos_framework::event::{Self, EventHandle}; use aptos_framework::reconfiguration; use aptos_framework::reconfiguration_with_dkg; + use aptos_framework::code; use aptos_framework::stake; use aptos_framework::state_storage; use aptos_framework::system_addresses; @@ -336,12 +337,18 @@ module aptos_framework::block { }; } + /// Runs at the end of a block. Records fee distribution and drains the code publish queue, + /// returning the packages (each a vector of module bytecode blobs) queued during the block. + /// The AptosVM materializes the returned packages into module writes. The return value is only + /// consumed when the deferred module publishing feature is enabled; otherwise the queue is + /// empty and an empty vector is returned. fun block_epilogue( vm: &signer, fee_distribution_validator_indices: vector, fee_amounts_octa: vector - ) { + ): vector>> { stake::record_fee(vm, fee_distribution_validator_indices, fee_amounts_octa); + code::drain_publish_queue() } #[view] diff --git a/aptos-move/framework/aptos-framework/sources/code.move b/aptos-move/framework/aptos-framework/sources/code.move index 44553d20313..32597cef7b8 100644 --- a/aptos-move/framework/aptos-framework/sources/code.move +++ b/aptos-move/framework/aptos-framework/sources/code.move @@ -16,6 +16,7 @@ module aptos_framework::code { use aptos_framework::permissioned_signer; friend aptos_framework::object_code_deployment; + friend aptos_framework::block; // ---------------------------------------------------------------------- // Code Publishing @@ -111,6 +112,28 @@ module aptos_framework::code { /// Current permissioned signer cannot publish codes. const ENO_CODE_PERMISSION: u64 = 0xB; + /// A package is already queued for publishing at this address in the current block. + const EALREADY_QUEUED: u64 = 0xC; + + /// Global index of code addresses with a package queued for publishing in the current block. + /// Only addresses are stored here; the bytecode is sharded into a per-address `QueuedPackage`. + /// Drained by the block epilogue, which materializes the queued packages into module writes. + /// Wrapped in an enum so the representation can change without a storage migration. + enum PublishQueue has key { + V1 { + pending: vector
, + } + } + + /// A single package awaiting materialization, stored at its code address. Constructed only by + /// `enqueue_package` after successful verification, so the block epilogue trusts its contents + /// and does not re-verify. + enum QueuedPackage has key { + V1 { + code: vector>, + } + } + struct CodePublishingPermission has copy, drop, store {} /// Permissions @@ -165,7 +188,7 @@ module aptos_framework::code { /// Publishes a package at the given signer's address. The caller must provide package metadata describing the /// package. - public fun publish_package(owner: &signer, pack: PackageMetadata, code: vector>) acquires PackageRegistry { + public fun publish_package(owner: &signer, pack: PackageMetadata, code: vector>) acquires PackageRegistry, PublishQueue { check_code_publishing_permission(owner); // Disallow incompatible upgrade mode. Governance can decide later if this should be reconsidered. assert!( @@ -218,12 +241,73 @@ module aptos_framework::code { }); // Request publish - if (features::code_dependency_check_enabled()) + if (features::is_deferred_module_publishing_enabled() && features::is_lazy_loading_enabled()) { + // Verify and charge for the package now, then queue the verified bytecode. The actual + // module writes are materialized at the block epilogue. Deferred publishing only links + // immediate dependencies, so it is coupled to lazy loading; the eager path keeps using + // the legacy publish flow, which charges the whole transitive dependency closure. + verify_package(addr, module_names, allowed_deps, copy code, policy.policy); + enqueue_package(owner, addr, code); + } else if (features::code_dependency_check_enabled()) { request_publish_with_allowed_deps(addr, module_names, allowed_deps, code, policy.policy) - else - // The new `request_publish_with_allowed_deps` has not yet rolled out, so call downwards - // compatible code. + } else { + // The new `request_publish_with_allowed_deps` has not yet rolled out, so call downwards + // compatible code. request_publish(addr, module_names, code, policy.policy) + } + } + + /// Initializes the publish queue index. Called when the deferred module publishing feature is + /// enabled (and at genesis), since a user transaction cannot create a resource at the framework + /// address. + public fun initialize_publish_queue(aptos_framework: &signer) { + system_addresses::assert_aptos_framework(aptos_framework); + if (!exists(@aptos_framework)) { + move_to(aptos_framework, PublishQueue::V1 { pending: vector::empty() }); + } + } + + /// Queues a verified package for publishing at the block epilogue. The bytecode is stored at + /// the code address (sharded per publisher); the address is recorded in the global index. + fun enqueue_package( + owner: &signer, + code_address: address, + code: vector>, + ) acquires PublishQueue { + // At most one package may be queued per address per block. + assert!(!exists(code_address), error::already_exists(EALREADY_QUEUED)); + move_to(owner, QueuedPackage::V1 { code }); + + let queue = &mut PublishQueue[@aptos_framework]; + match (queue) { + PublishQueue::V1 { pending } => pending.push_back(code_address), + } + } + + /// Drains the publish queue, returning the queued packages (each a vector of module bytecode + /// blobs). Called by the block epilogue; the AptosVM materializes the returned packages into + /// module writes. Returns an empty vector if the queue has not been initialized. + public(friend) fun drain_publish_queue(): vector>> + acquires PublishQueue, QueuedPackage { + if (!exists(@aptos_framework)) { + return vector[] + }; + let queue = &mut PublishQueue[@aptos_framework]; + let addresses = match (queue) { + PublishQueue::V1 { pending } => { + let addresses = *pending; + *pending = vector[]; + addresses + } + }; + let packages = vector[]; + addresses.for_each(|code_address| { + let code = match (move_from(code_address)) { + QueuedPackage::V1 { code } => code, + }; + packages.push_back(code); + }); + packages } public fun freeze_code_object(publisher: &signer, code_object: Object) acquires PackageRegistry { @@ -253,7 +337,7 @@ module aptos_framework::code { /// Same as `publish_package` but as an entry function which can be called as a transaction. Because /// of current restrictions for txn parameters, the metadata needs to be passed in serialized form. public entry fun publish_package_txn(owner: &signer, metadata_serialized: vector, code: vector>) - acquires PackageRegistry { + acquires PackageRegistry, PublishQueue { publish_package(owner, util::from_bytes(metadata_serialized), code) } @@ -368,6 +452,18 @@ module aptos_framework::code { policy: u8 ); + /// Native function that verifies a package (compatibility, bytecode verification, linking, + /// metadata / resource-group / event checks) and charges the associated gas, aborting on any + /// failure. Used by the deferred publishing flow: the verified bytecode is queued and the + /// module writes are materialized at the block epilogue. + native fun verify_package( + owner: address, + expected_modules: vector, + allowed_deps: vector, + bundle: vector>, + policy: u8 + ); + /// A helper type for request_publish_with_allowed_deps struct AllowedDep has drop { /// Address of the module. diff --git a/aptos-move/framework/aptos-framework/sources/genesis.move b/aptos-move/framework/aptos-framework/sources/genesis.move index bb6d2447c82..f0b7be05c5a 100644 --- a/aptos-move/framework/aptos-framework/sources/genesis.move +++ b/aptos-move/framework/aptos-framework/sources/genesis.move @@ -13,6 +13,7 @@ module aptos_framework::genesis { use aptos_framework::block; use aptos_framework::chain_id; use aptos_framework::chain_status; + use aptos_framework::code; use aptos_framework::coin; use aptos_framework::consensus_config; use aptos_framework::execution_config; @@ -133,6 +134,7 @@ module aptos_framework::genesis { block::initialize(&aptos_framework_account, epoch_interval_microsecs); state_storage::initialize(&aptos_framework_account); nonce_validation::initialize(&aptos_framework_account); + code::initialize_publish_queue(&aptos_framework_account); transaction_limits::initialize( &aptos_framework_account, diff --git a/aptos-move/framework/cached-packages/src/head.mrb b/aptos-move/framework/cached-packages/src/head.mrb index daec45ada0b18640b1b486a3361c305d095f25b2..ff7cb7381c2963228920623e3b0f8d9c6aaf9fde 100644 GIT binary patch delta 28150 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 SJS^~tfk(ViGlpGW^M3#@zE5EQ delta 25860 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, + * allowed_deps: vector, + * bundle: vector>, + * policy: u8 + * ) + * + * Runs the same verification and gas charging the AptosVM performs for a published bundle + * (compatibility, bytecode verification, linking, metadata / resource-group / event checks), + * but from within the publishing transaction. Aborts on any failure. Does not run + * `init_module` and does not materialize module writes: the verified bytecode is written to a + * queue resource by the caller and materialized at the block epilogue. + * + **************************************************************************************************/ +fn native_verify_package( + context: &mut SafeNativeContext, + _ty_args: &[Type], + mut args: VecDeque, +) -> SafeNativeResult> { + debug_assert_eq!(args.len(), 5); + + context.charge(CODE_REQUEST_PUBLISH_BASE)?; + + // The policy is only used by the Move caller to update the package registry; it is + // deliberately ignored here (arbitrary upgrade policy is rejected before this native is + // called, so compatibility is always checked). + let _policy = safely_pop_arg!(args, u8); + + let mut code = vec![]; + for module in safely_pop_arg!(args, Vec) { + let module_code = module.value_as::>()?; + context.charge(CODE_REQUEST_PUBLISH_PER_BYTE * NumBytes::new(module_code.len() as u64))?; + code.push(module_code); + } + + let mut allowed_deps: BTreeMap> = BTreeMap::new(); + for dep in safely_pop_arg!(args, Vec) { + let (account, module_name) = unpack_allowed_dep(dep)?; + let entry = allowed_deps.entry(account); + if let Entry::Vacant(_) = &entry { + context.charge(CODE_REQUEST_PUBLISH_PER_BYTE * NumBytes::new(32))?; + } + context.charge(CODE_REQUEST_PUBLISH_PER_BYTE * NumBytes::new(module_name.len() as u64))?; + entry.or_default().insert(module_name); + } + + let mut expected_modules = BTreeSet::new(); + for name in safely_pop_arg!(args, Vec) { + let str = get_move_string(name)?; + context.charge(CODE_REQUEST_PUBLISH_PER_BYTE * NumBytes::new(str.len() as u64))?; + expected_modules.insert(str); + } + + let destination = safely_pop_arg!(args, AccountAddress); + + // Gather everything derived from the context before taking the mutable metering borrow. + let features = context.get_feature_flags().clone(); + + // A package may always depend on its own modules. + allowed_deps + .entry(destination) + .or_default() + .extend(expected_modules.clone()); + // The allowed-dependency check is only enforced when CODE_DEPENDENCY_CHECK is enabled, matching + // the legacy publish path (request_publish vs request_publish_with_allowed_deps). + let allowed_deps = features + .is_enabled(FeatureFlag::CODE_DEPENDENCY_CHECK) + .then_some(allowed_deps); + + let gas_feature_version = context.gas_feature_version(); + let treat_entry_as_public = context.timed_feature_enabled(TimedFeatureFlag::EntryCompatibility); + let is_mainnet = { + let txn_context = context.extensions().get::(); + ChainId::new(txn_context.chain_id()).is_mainnet() + }; + + let compatibility = Compatibility::new( + true, + !features.is_enabled(FeatureFlag::TREAT_FRIEND_AS_PRIVATE), + treat_entry_as_public, + gas_feature_version < gas_feature_versions::RELEASE_V1_34, + features.is_enabled(FeatureFlag::ALLOW_FRIEND_ENTRY_VISIBILITY_DOWNGRADE), + ); + + let mut modules = Vec::with_capacity(code.len()); + { + let (module_storage, traversal_context, gas_meter) = + context.module_storage_wrapper_with_metering(); + + // Deserialize the bundle for metadata / resource-group / event validation. + { + let deserializer_config = &module_storage + .runtime_environment() + .vm_config() + .deserializer_config; + for blob in &code { + let module = CompiledModule::deserialize_with_config(blob, deserializer_config) + .map_err(|_| { + SafeNativeError::InvariantViolation(PartialVMError::new( + StatusCode::CODE_DESERIALIZATION_ERROR, + )) + })?; + modules.push(module); + } + } + + // Charge dependency gas for the old (upgraded) and new versions of each module, matching + // the legacy publish path. `charge_dependency` is a no-op for special addresses and older + // gas feature versions. + for (module, blob) in modules.iter().zip(code.iter()) { + let addr = module.self_addr(); + let name = module.self_name(); + let old_size = module_storage + .unmetered_get_module_size(addr, name) + .map_err(|err| SafeNativeError::InvariantViolation(err.to_partial()))?; + if let Some(old_size) = old_size { + gas_meter + .charge_dependency( + DependencyKind::Existing, + addr, + name, + NumBytes::new(old_size as u64), + ) + .map_err(SafeNativeError::InvariantViolation)?; + } + gas_meter + .charge_dependency( + DependencyKind::New, + addr, + name, + NumBytes::new(blob.len() as u64), + ) + .map_err(SafeNativeError::InvariantViolation)?; + + // Mark the modules in the bundle as visited so that validation (resource groups, events) + // and later checks do not treat them as unmetered accesses. + traversal_context.visit_if_not_special_module_id(&module.self_id()); + } + + // Deferred publishing is only reachable under lazy loading (see `code.move`), which links + // and charges immediate dependencies rather than the whole transitive closure. This mirrors + // the lazy branch of the legacy `charge_package_dependencies`. + let module_ids_in_bundle = modules + .iter() + .map(|module| (module.self_addr(), module.self_name())) + .collect::>(); + + // Charge gas for immediate dependencies that are not part of the bundle. + for (dep_addr, dep_name) in modules + .iter() + .flat_map(|module| module.immediate_dependencies_iter()) + .filter(|addr_and_name| !module_ids_in_bundle.contains(addr_and_name)) + { + let module_id = ModuleId::new(*dep_addr, dep_name.to_owned()); + if traversal_context.visit_if_not_special_module_id(&module_id) { + let size = module_storage + .unmetered_get_existing_module_size(dep_addr, dep_name) + .map_err(|err| SafeNativeError::InvariantViolation(err.to_partial()))?; + gas_meter + .charge_dependency( + DependencyKind::Existing, + dep_addr, + dep_name, + NumBytes::new(size as u64), + ) + .map_err(SafeNativeError::InvariantViolation)?; + } + } + + // A friend of a published module must belong to the same bundle. + for (friend_addr, friend_name) in modules + .iter() + .flat_map(|module| module.immediate_friends_iter()) + { + if !module_ids_in_bundle.contains(&(friend_addr, friend_name)) { + let msg = format!( + "Module {}::{} is declared as a friend and should be part of the module \ + bundle, but it is not", + friend_addr, friend_name + ); + return Err(SafeNativeError::InvariantViolation( + PartialVMError::new(StatusCode::FRIEND_NOT_FOUND_IN_MODULE_BUNDLE) + .with_message(msg), + )); + } + } + + // Module complexity check (matches the legacy publish path). + for (module, blob) in modules.iter().zip(code.iter()) { + // TODO(Gas): Make budget configurable. + let budget = 2048 + blob.len() as u64 * 20; + check_module_complexity(module, budget).map_err(SafeNativeError::InvariantViolation)?; + } + + // Aptos-specific validation: unstable bytecode, native functions, expected modules, allowed + // dependencies, module metadata, resource groups, and events. + validate_publish_request( + &features, + is_mainnet, + &module_storage, + traversal_context, + gas_meter, + &modules, + expected_modules, + allowed_deps, + ) + .map_err(|err| SafeNativeError::InvariantViolation(err.to_partial()))?; + + // Bytecode verification, address check, compatibility, and linking against dependencies. + StagingModuleStorage::create_with_compat_config( + &destination, + compatibility, + &module_storage, + code.into_iter().map(Bytes::from).collect(), + ) + .map_err(|err| SafeNativeError::InvariantViolation(err.to_partial()))?; + } + + // Pre-charge I/O for the module writes that are materialized at the block epilogue. Only the + // state slot and key are charged here; the value bytes are covered by the queue resource write. + for module in &modules { + let state_key = StateKey::module_id(&module.self_id()); + context.charge_io_gas_for_write(NumBytes::new(state_key.size() as u64))?; + } + + Ok(smallvec![]) +} + /*************************************************************************************************** * module * @@ -369,6 +617,7 @@ pub fn make_all( let natives = [ ("request_publish", native_request_publish as RawSafeNative), ("request_publish_with_allowed_deps", native_request_publish), + ("verify_package", native_verify_package), ]; builder.make_named_natives(natives) diff --git a/aptos-move/framework/natives/src/lib.rs b/aptos-move/framework/natives/src/lib.rs index 165a309868e..fa2e83ae4bb 100644 --- a/aptos-move/framework/natives/src/lib.rs +++ b/aptos-move/framework/natives/src/lib.rs @@ -19,6 +19,7 @@ pub mod hash; pub mod object; pub mod object_code_deployment; pub mod permissioned_signer; +pub mod publish_verification; pub mod randomness; pub mod state_storage; pub mod storage_slot; diff --git a/aptos-move/aptos-vm/src/verifier/event_validation.rs b/aptos-move/framework/natives/src/publish_verification/event_validation.rs similarity index 97% rename from aptos-move/aptos-vm/src/verifier/event_validation.rs rename to aptos-move/framework/natives/src/publish_verification/event_validation.rs index 0961b224ca3..f7a059f47dc 100644 --- a/aptos-move/aptos-vm/src/verifier/event_validation.rs +++ b/aptos-move/framework/natives/src/publish_verification/event_validation.rs @@ -35,7 +35,7 @@ fn metadata_validation_error(msg: &str) -> VMError { /// Validate event metadata on modules one by one: /// * Extract the event metadata /// * Verify all changes are compatible upgrades (existing event attributes cannot be removed) -pub(crate) fn validate_module_events( +pub fn validate_module_events( features: &Features, module_storage: &impl ModuleStorage, traversal_context: &TraversalContext, @@ -97,7 +97,7 @@ pub(crate) fn validate_module_events( /// 0x1::event::emit(); /// } /// ``` -pub(crate) fn validate_emit_calls( +pub fn validate_emit_calls( event_structs: &HashSet, module: &CompiledModule, ) -> VMResult<()> { @@ -255,9 +255,7 @@ pub(crate) fn validate_emit_calls( } /// Given a module id extract all event metadata -pub(crate) fn extract_event_metadata( - metadata: &RuntimeModuleMetadataV1, -) -> VMResult> { +pub fn extract_event_metadata(metadata: &RuntimeModuleMetadataV1) -> VMResult> { let mut event_structs = HashSet::new(); for (struct_, attrs) in &metadata.struct_attributes { for attr in attrs { @@ -288,7 +286,7 @@ pub(crate) fn extract_event_metadata( /// ``` /// /// This is ok to fail here, as event emission should be done by the module where event is defined. -pub(crate) fn verify_no_event_emission_in_compiled_script(script: &CompiledScript) -> VMResult<()> { +pub fn verify_no_event_emission_in_compiled_script(script: &CompiledScript) -> VMResult<()> { for func_handle in &script.function_handles { if is_event_emit_call(BinaryIndexedView::Script(script), func_handle) { debug_assert!(func_handle.type_parameters.len() == 1); diff --git a/aptos-move/framework/natives/src/publish_verification/mod.rs b/aptos-move/framework/natives/src/publish_verification/mod.rs new file mode 100644 index 00000000000..9db9c2e9e92 --- /dev/null +++ b/aptos-move/framework/natives/src/publish_verification/mod.rs @@ -0,0 +1,11 @@ +// Copyright (c) Aptos Foundation +// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE + +//! Module-publishing verification shared between AptosVM's legacy publish path +//! and the `code::verify_package` native. Kept in the framework natives crate so +//! the native can reuse it (AptosVM cannot be a dependency of the natives crate). + +pub mod event_validation; +pub mod native_validation; +pub mod publish; +pub mod resource_groups; diff --git a/aptos-move/aptos-vm/src/verifier/native_validation.rs b/aptos-move/framework/natives/src/publish_verification/native_validation.rs similarity index 92% rename from aptos-move/aptos-vm/src/verifier/native_validation.rs rename to aptos-move/framework/natives/src/publish_verification/native_validation.rs index fd8eb9c093e..e0b1514d21c 100644 --- a/aptos-move/aptos-vm/src/verifier/native_validation.rs +++ b/aptos-move/framework/natives/src/publish_verification/native_validation.rs @@ -9,7 +9,7 @@ use move_binary_format::{ use move_core_types::vm_status::StatusCode; /// Validate that only system address can publish new non-entry natives. -pub(crate) fn validate_module_natives(modules: &[CompiledModule]) -> VMResult<()> { +pub fn validate_module_natives(modules: &[CompiledModule]) -> VMResult<()> { for module in modules { let module_address = module.self_addr(); for native in module.function_defs().iter().filter(|def| def.is_native()) { diff --git a/aptos-move/framework/natives/src/publish_verification/publish.rs b/aptos-move/framework/natives/src/publish_verification/publish.rs new file mode 100644 index 00000000000..b9fb9b7125f --- /dev/null +++ b/aptos-move/framework/natives/src/publish_verification/publish.rs @@ -0,0 +1,108 @@ +// Copyright (c) Aptos Foundation +// Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE + +//! Shared module-publishing validation, reused by AptosVM's legacy publish path +//! and by the `code::verify_package` native. This performs the Aptos-specific +//! checks (unstable bytecode, native functions, expected modules / allowed +//! dependencies, module metadata, resource groups, events). The bytecode +//! verification, compatibility, and linking checks live in `StagingModuleStorage` +//! (move-vm-runtime). + +use crate::publish_verification::{event_validation, native_validation, resource_groups}; +use aptos_types::{ + on_chain_config::Features, + vm::module_metadata::{get_compilation_metadata, verify_module_metadata_for_module_publishing}, +}; +use move_binary_format::{ + access::ModuleAccess, + errors::{Location, PartialVMError, VMError, VMResult}, + CompiledModule, +}; +use move_core_types::{account_address::AccountAddress, vm_status::StatusCode}; +use move_vm_runtime::{module_traversal::TraversalContext, ModuleStorage}; +use move_vm_types::gas::DependencyGasMeter; +use std::collections::{BTreeMap, BTreeSet}; + +pub fn metadata_validation_error(msg: &str) -> VMError { + PartialVMError::new(StatusCode::CONSTRAINT_NOT_SATISFIED) + .with_message(format!("metadata and code bundle mismatch: {}", msg)) + .finish(Location::Undefined) +} + +/// Check whether the bytecode can be published to mainnet based on the unstable +/// tag in the compilation metadata. +pub fn reject_unstable_bytecode(is_mainnet: bool, modules: &[CompiledModule]) -> VMResult<()> { + if is_mainnet { + for module in modules { + if let Some(metadata) = get_compilation_metadata(module) { + if metadata.unstable { + return Err(PartialVMError::new(StatusCode::UNSTABLE_BYTECODE_REJECTED) + .with_message( + "code marked unstable is not published on mainnet".to_string(), + ) + .finish(Location::Undefined)); + } + } + } + } + Ok(()) +} + +/// Validate a publish request: reject unstable bytecode, validate native +/// functions, check the modules against the expected-module set and the +/// allowed-dependency whitelist, validate module metadata, resource groups, and +/// events. +pub fn validate_publish_request( + features: &Features, + is_mainnet: bool, + module_storage: &impl ModuleStorage, + traversal_context: &mut TraversalContext, + gas_meter: &mut dyn DependencyGasMeter, + modules: &[CompiledModule], + mut expected_modules: BTreeSet, + allowed_deps: Option>>, +) -> VMResult<()> { + reject_unstable_bytecode(is_mainnet, modules)?; + native_validation::validate_module_natives(modules)?; + + for m in modules { + if !expected_modules.remove(m.self_id().name().as_str()) { + return Err(metadata_validation_error(&format!( + "unregistered module: '{}'", + m.self_id().name() + ))); + } + if let Some(allowed) = &allowed_deps { + for dep in m.immediate_dependencies() { + if !allowed + .get(dep.address()) + .map(|modules| modules.contains("") || modules.contains(dep.name().as_str())) + .unwrap_or(false) + { + return Err(metadata_validation_error(&format!( + "unregistered dependency: '{}'", + dep + ))); + } + } + } + verify_module_metadata_for_module_publishing(m, features) + .map_err(|err| metadata_validation_error(&err.to_string()))?; + } + + resource_groups::validate_resource_groups( + features, + module_storage, + traversal_context, + gas_meter, + modules, + )?; + event_validation::validate_module_events(features, module_storage, traversal_context, modules)?; + + if !expected_modules.is_empty() { + return Err(metadata_validation_error( + "not all registered modules published", + )); + } + Ok(()) +} diff --git a/aptos-move/aptos-vm/src/verifier/resource_groups.rs b/aptos-move/framework/natives/src/publish_verification/resource_groups.rs similarity index 97% rename from aptos-move/aptos-vm/src/verifier/resource_groups.rs rename to aptos-move/framework/natives/src/publish_verification/resource_groups.rs index 23c8671c0bf..d9cdae347b3 100644 --- a/aptos-move/aptos-vm/src/verifier/resource_groups.rs +++ b/aptos-move/framework/natives/src/publish_verification/resource_groups.rs @@ -14,7 +14,7 @@ use move_binary_format::{ }; use move_core_types::{gas_algebra::NumBytes, language_storage::StructTag, vm_status::StatusCode}; use move_vm_runtime::{module_traversal::TraversalContext, ModuleStorage}; -use move_vm_types::gas::{DependencyKind, GasMeter}; +use move_vm_types::gas::{DependencyGasMeter, DependencyKind}; use std::collections::{BTreeMap, BTreeSet}; fn metadata_validation_err(msg: &str) -> Result<(), VMError> { @@ -33,11 +33,11 @@ fn metadata_validation_error(msg: &str) -> VMError { /// * Ensure that each member has a membership and it does not change /// * Ensure that each group has a scope and that it does not become more restrictive /// * For any new members, verify that they are in a valid resource group -pub(crate) fn validate_resource_groups( +pub fn validate_resource_groups( features: &Features, module_storage: &impl ModuleStorage, traversal_context: &mut TraversalContext, - gas_meter: &mut impl GasMeter, + gas_meter: &mut dyn DependencyGasMeter, new_modules: &[CompiledModule], ) -> Result<(), VMError> { let mut groups = BTreeMap::new(); @@ -107,7 +107,7 @@ pub(crate) fn validate_resource_groups( /// * Extract the resource group metadata /// * Verify all changes are compatible upgrades /// * Return any new members to validate correctness and all groups to assist in validation -pub(crate) fn validate_module_and_extract_new_entries( +pub fn validate_module_and_extract_new_entries( module_storage: &impl ModuleStorage, new_module: &CompiledModule, features: &Features, @@ -189,7 +189,7 @@ pub(crate) fn validate_module_and_extract_new_entries( } /// Given a module id extract all resource group metadata -pub(crate) fn extract_resource_group_metadata_from_module( +pub fn extract_resource_group_metadata_from_module( old_module: &CompiledModule, ) -> VMResult<( BTreeMap, @@ -213,7 +213,7 @@ pub(crate) fn extract_resource_group_metadata_from_module( } /// Given a module id extract all resource group metadata -pub(crate) fn extract_resource_group_metadata( +pub fn extract_resource_group_metadata( metadata: &RuntimeModuleMetadataV1, ) -> VMResult<( BTreeMap, diff --git a/consensus/src/pipeline/pipeline_builder.rs b/consensus/src/pipeline/pipeline_builder.rs index 3ecae0f624c..33195cedbe1 100644 --- a/consensus/src/pipeline/pipeline_builder.rs +++ b/consensus/src/pipeline/pipeline_builder.rs @@ -591,6 +591,7 @@ impl PipelineBuilder { Self::execute( prepare_fut.clone(), parent.execute_fut.clone(), + parent.pre_commit_fut.clone(), rand_check_fut.clone(), self.executor.clone(), block.clone(), @@ -931,6 +932,7 @@ impl PipelineBuilder { async fn execute( prepare_fut: TaskFuture, parent_block_execute_fut: TaskFuture, + parent_block_pre_commit_fut: TaskFuture, rand_check: TaskFuture, executor: Arc, block: Arc, @@ -940,6 +942,12 @@ impl PipelineBuilder { ) -> TaskResult { let mut tracker = Tracker::start_waiting("execute", &block); parent_block_execute_fut.await?; + // Deferred module publishing barrier: if the parent block published modules, wait for it to + // be pre-committed (which invalidates those modules in the cross-block module cache) before + // executing this block, so this block does not execute against stale cached code. + if executor.block_has_published_modules(block.parent_id()) { + parent_block_pre_commit_fut.await?; + } let (user_txns, block_gas_limit, decryption_outcome) = prepare_fut.await?; let onchain_execution_config = onchain_execution_config.with_block_gas_limit_override(block_gas_limit); diff --git a/execution/executor-types/Cargo.toml b/execution/executor-types/Cargo.toml index 5d39dca8c85..2a60dcfe6fd 100644 --- a/execution/executor-types/Cargo.toml +++ b/execution/executor-types/Cargo.toml @@ -33,6 +33,7 @@ bon = { workspace = true } criterion = { workspace = true } derive_more = { workspace = true } itertools = { workspace = true } +move-core-types = { workspace = true } once_cell = { workspace = true } ouroboros = { workspace = true } rayon = { workspace = true } diff --git a/execution/executor-types/src/execution_output.rs b/execution/executor-types/src/execution_output.rs index 96ecab97a66..9a21b6e27ed 100644 --- a/execution/executor-types/src/execution_output.rs +++ b/execution/executor-types/src/execution_output.rs @@ -20,6 +20,7 @@ use aptos_types::{ }, }; use derive_more::Deref; +use move_core_types::language_storage::ModuleId; use std::sync::Arc; #[derive(Clone, Debug, Deref)] @@ -43,6 +44,7 @@ impl ExecutionOutput { hot_state_updates: HotStateUpdates, block_end_info: Option, next_epoch_state: Option, + published_modules: Vec, subscribable_events: Planned>, transaction_info_v1: bool, hot_state_root_in_txn_info: bool, @@ -71,6 +73,7 @@ impl ExecutionOutput { hot_state_updates, block_end_info, next_epoch_state, + published_modules, subscribable_events, transaction_info_v1, hot_state_root_in_txn_info, @@ -91,6 +94,7 @@ impl ExecutionOutput { hot_state_updates: HotStateUpdates::new_empty(), block_end_info: None, next_epoch_state: None, + published_modules: vec![], subscribable_events: Planned::ready(vec![]), transaction_info_v1: false, hot_state_root_in_txn_info: false, @@ -113,6 +117,7 @@ impl ExecutionOutput { hot_state_updates: HotStateUpdates::new_empty(), block_end_info: None, next_epoch_state: None, + published_modules: vec![], subscribable_events: Planned::ready(vec![]), transaction_info_v1: false, hot_state_root_in_txn_info: false, @@ -137,6 +142,7 @@ impl ExecutionOutput { hot_state_updates: HotStateUpdates::new_empty(), block_end_info: None, next_epoch_state: self.next_epoch_state.clone(), + published_modules: vec![], subscribable_events: Planned::ready(vec![]), transaction_info_v1: self.transaction_info_v1, hot_state_root_in_txn_info: self.hot_state_root_in_txn_info, @@ -189,6 +195,10 @@ pub struct Inner { /// Only present if the block is the last block of an epoch, and is parsed output of the /// state cache. pub next_epoch_state: Option, + /// Modules published by the block epilogue (deferred module publishing). The global module + /// cache is invalidated for these modules at commit time so the next block reads the new code. + /// Empty when the feature is disabled or no modules were published. + pub published_modules: Vec, pub subscribable_events: Planned>, /// Whether to assemble `TransactionInfoV1` (instead of `TransactionInfoV0`) in the /// subsequent state-checkpoint / ledger-update phases. diff --git a/execution/executor-types/src/lib.rs b/execution/executor-types/src/lib.rs index 219ed2c4d11..f5e072ccdad 100644 --- a/execution/executor-types/src/lib.rs +++ b/execution/executor-types/src/lib.rs @@ -175,6 +175,14 @@ pub trait BlockExecutorTrait: Send + Sync { fn finish(&self); fn state_view(&self, block_id: HashValue) -> ExecutorResult; + + /// Returns whether the given (already executed) block published any modules via the deferred + /// module publishing flow. Used by the consensus pipeline to make a publishing block a commit + /// barrier (the next block does not execute until this one is pre-committed and its published + /// modules are invalidated in the module cache). Defaults to `false`. + fn block_has_published_modules(&self, _block_id: HashValue) -> bool { + false + } } #[derive(Clone)] diff --git a/execution/executor/src/block_executor/mod.rs b/execution/executor/src/block_executor/mod.rs index ec5ec21c5e2..42ba760f9f9 100644 --- a/execution/executor/src/block_executor/mod.rs +++ b/execution/executor/src/block_executor/mod.rs @@ -165,6 +165,13 @@ where self.maybe_initialize()?; self.inner.read().as_ref().unwrap().state_view(block_id) } + + fn block_has_published_modules(&self, block_id: HashValue) -> bool { + self.inner + .read() + .as_ref() + .is_some_and(|inner| inner.block_has_published_modules(block_id)) + } } struct BlockExecutorInner { @@ -409,6 +416,16 @@ where TRANSACTIONS_SAVED.observe(num_txns as f64); } + // Invalidate the modules published by this block (deferred module publishing) in the block + // executor's cross-block module cache, so the next block re-reads the new code from storage. + // The publishing barrier ensures the next block does not execute against a stale cache + // before this runs. + let published_modules = block.output.execution_output.published_modules.clone(); + if !published_modules.is_empty() { + self.block_executor + .invalidate_published_modules(published_modules); + } + Ok(()) } @@ -459,4 +476,10 @@ where block.output.result_state().latest().clone(), )?) } + + fn block_has_published_modules(&self, block_id: HashValue) -> bool { + self.block_tree + .get_block(block_id) + .is_ok_and(|block| !block.output.execution_output.published_modules.is_empty()) + } } diff --git a/execution/executor/src/workflow/do_get_execution_output.rs b/execution/executor/src/workflow/do_get_execution_output.rs index c9d96f3c823..3a94d6f5ce6 100644 --- a/execution/executor/src/workflow/do_get_execution_output.rs +++ b/execution/executor/src/workflow/do_get_execution_output.rs @@ -29,6 +29,7 @@ use aptos_storage_interface::state_store::{ }; #[cfg(feature = "consensus-only-perf-test")] use aptos_types::transaction::ExecutionStatus; +use move_core_types::language_storage::ModuleId; use aptos_types::{ block_executor::{ config::BlockExecutorConfigFromOnchain, @@ -120,6 +121,7 @@ impl DoGetExecutionOutput { onchain_config, transaction_slice_metadata, )?; + let published_modules = block_output.published_modules().to_vec(); let (mut transaction_outputs, block_epilogue_txn) = block_output.into_inner(); let (transactions, mut auxiliary_infos) = txn_provider.into_inner(); let mut transactions = transactions @@ -187,6 +189,7 @@ impl DoGetExecutionOutput { .transaction_info_v1(onchain_config.transaction_info_v1()) .hot_state_root_in_txn_info(onchain_config.hot_state_root_in_txn_info()) .compute_trading_native_state_roots(onchain_config.compute_trading_native_state_roots()) + .published_modules(published_modules) .build() } @@ -232,6 +235,8 @@ impl DoGetExecutionOutput { .transaction_info_v1(onchain_config.transaction_info_v1()) .hot_state_root_in_txn_info(onchain_config.hot_state_root_in_txn_info()) .compute_trading_native_state_roots(onchain_config.compute_trading_native_state_roots()) + // Sharded execution does not use the deferred module publishing flow. + .published_modules(vec![]) .build() } @@ -261,6 +266,9 @@ impl DoGetExecutionOutput { .transaction_info_v1(onchain_config.transaction_info_v1()) .hot_state_root_in_txn_info(onchain_config.hot_state_root_in_txn_info()) .compute_trading_native_state_roots(onchain_config.compute_trading_native_state_roots()) + // The state-sync/replay-from-outputs path does not carry structured module ids; the + // module cache is refreshed via check_ready on non-consecutive slices instead. + .published_modules(vec![]) .build()?; let ret = out.clone(); @@ -379,6 +387,7 @@ impl Parser { transaction_info_v1: bool, hot_state_root_in_txn_info: bool, compute_trading_native_state_roots: bool, + published_modules: Vec, ) -> Result { let _timer = OTHER_TIMERS.timer_with(&["parse_raw_output"]); @@ -465,6 +474,7 @@ impl Parser { .hot_state_updates(hot_state_updates) .maybe_block_end_info(block_end_info) .maybe_next_epoch_state(next_epoch_state) + .published_modules(published_modules) .subscribable_events(Planned::place_holder()) .transaction_info_v1(transaction_info_v1) .hot_state_root_in_txn_info(hot_state_root_in_txn_info) @@ -665,6 +675,7 @@ mod tests { .transaction_info_v1(false) .hot_state_root_in_txn_info(false) .compute_trading_native_state_roots(false) + .published_modules(vec![]) .build() .unwrap(); assert_eq!( diff --git a/third_party/move/move-vm/runtime/src/native_functions.rs b/third_party/move/move-vm/runtime/src/native_functions.rs index fade234fbd5..391c682c577 100644 --- a/third_party/move/move-vm/runtime/src/native_functions.rs +++ b/third_party/move/move-vm/runtime/src/native_functions.rs @@ -141,6 +141,26 @@ impl<'a, 'b, 'c> NativeContext<'a, 'b, 'c> { traversal_context, } } + + /// Returns a `ModuleStorageWrapper` over the underlying module storage together with mutable + /// access to the traversal context and a dependency gas meter. This lets a native function + /// stage and verify a module bundle (e.g. `code::verify_package`) using the standard staging + /// and validation machinery, which requires a concrete `impl ModuleStorage` and a + /// `DependencyGasMeter`. The wrapper owns a copy of the `'a` module-storage reference, so it + /// does not borrow `self` and can outlive the returned mutable borrows. + pub fn module_storage_wrapper_with_metering( + &mut self, + ) -> ( + ModuleStorageWrapper<'a>, + &mut TraversalContext<'c>, + &mut dyn DependencyGasMeter, + ) { + ( + ModuleStorageWrapper::new(self.module_storage), + self.traversal_context, + self.gas_meter, + ) + } } impl<'b, 'c> NativeContext<'_, 'b, 'c> { @@ -508,10 +528,20 @@ impl<'a, 'b> LoaderContext<'a, 'b> { } // Wrappers to use trait objects where static dispatch is expected. -struct ModuleStorageWrapper<'a> { +pub struct ModuleStorageWrapper<'a> { module_storage: &'a dyn ModuleStorage, } +impl<'a> ModuleStorageWrapper<'a> { + /// Wraps a `&dyn ModuleStorage` so it can be used where a concrete `impl ModuleStorage` is + /// required. This is needed by native functions that stage and verify a module bundle (e.g. + /// `code::verify_package`), because the staging machinery is generic over `M: ModuleStorage` + /// and a native only has a trait object. + pub fn new(module_storage: &'a dyn ModuleStorage) -> Self { + Self { module_storage } + } +} + impl<'a> LayoutCache for ModuleStorageWrapper<'a> { fn get_struct_layout(&self, key: &StructKey) -> Option { self.module_storage.get_struct_layout(key) diff --git a/third_party/move/move-vm/types/src/gas.rs b/third_party/move/move-vm/types/src/gas.rs index 9e3dec6ffbe..05fbe867239 100644 --- a/third_party/move/move-vm/types/src/gas.rs +++ b/third_party/move/move-vm/types/src/gas.rs @@ -205,6 +205,14 @@ pub trait NativeGasMeter: DependencyGasMeter { /// Tracks heap memory usage. fn use_heap_memory_in_native_context(&mut self, amount: u64) -> PartialVMResult<()>; + + /// Charges I/O gas for a write of a state slot whose key and value together occupy + /// `num_bytes` bytes. Used by natives that reserve I/O for a write that is materialized later + /// (e.g. `code::verify_package`, which pre-charges the module-write I/O done at the block + /// epilogue). Defaults to a no-op for meters that do not track I/O gas. + fn charge_io_gas_for_native_write(&mut self, _num_bytes: NumBytes) -> PartialVMResult<()> { + Ok(()) + } } /// Trait that defines a generic gas meter interface, allowing clients of the Move VM to implement diff --git a/types/src/on_chain_config/aptos_features.rs b/types/src/on_chain_config/aptos_features.rs index 22f90717c79..333faf9953f 100644 --- a/types/src/on_chain_config/aptos_features.rs +++ b/types/src/on_chain_config/aptos_features.rs @@ -205,6 +205,11 @@ pub enum FeatureFlag { /// When enabled, execution populates `TransactionInfoV1`'s hot state root hash, so it /// is committed to the ledger accumulator. Requires `TRANSACTION_INFO_V1`. HOT_STATE_ROOT_IN_TXN_INFO = 123, + /// When enabled, module publishing verifies and charges gas in the publishing + /// transaction, then queues the verified bytecode as a resource. The actual + /// module writes are materialized at the block epilogue. Published code becomes + /// active only from the next block. + DEFERRED_MODULE_PUBLISHING = 124, } impl FeatureFlag { @@ -321,6 +326,7 @@ impl FeatureFlag { Self::ALLOW_FRIEND_ENTRY_VISIBILITY_DOWNGRADE, Self::HOTNESS_IN_EPILOGUE, Self::ENCRYPTED_TRANSACTIONS, + Self::DEFERRED_MODULE_PUBLISHING, ] } } @@ -564,6 +570,10 @@ impl Features { self.is_enabled(FeatureFlag::HOT_STATE_ROOT_IN_TXN_INFO) } + pub fn is_deferred_module_publishing_enabled(&self) -> bool { + self.is_enabled(FeatureFlag::DEFERRED_MODULE_PUBLISHING) + } + pub fn get_max_identifier_size(&self) -> u64 { if self.is_enabled(FeatureFlag::LIMIT_MAX_IDENTIFIER_LENGTH) { IDENTIFIER_SIZE_MAX diff --git a/types/src/transaction/block_output.rs b/types/src/transaction/block_output.rs index af762c8b5f3..75c79b709b9 100644 --- a/types/src/transaction/block_output.rs +++ b/types/src/transaction/block_output.rs @@ -2,6 +2,7 @@ // Licensed pursuant to the Innovation-Enabling Source Code License, available at https://github.com/aptos-labs/aptos-core/blob/main/LICENSE use super::BlockExecutableTransaction; +use move_core_types::language_storage::ModuleId; use std::fmt::Debug; #[derive(Debug)] @@ -14,6 +15,10 @@ where // A BlockEpilogueTxn might be appended to the block. // This field will be None iff the input is not a block, or an epoch change is triggered. block_epilogue_txn: Option, + // Modules published by the block epilogue (deferred module publishing). Recorded so the + // global module cache can be invalidated for these modules at commit time. Empty when the + // feature is disabled or no modules were published. + published_modules: Vec, } impl BlockOutput @@ -25,9 +30,19 @@ where Self { transaction_outputs, block_epilogue_txn, + published_modules: vec![], } } + pub fn with_published_modules(mut self, published_modules: Vec) -> Self { + self.published_modules = published_modules; + self + } + + pub fn published_modules(&self) -> &[ModuleId] { + &self.published_modules + } + pub fn into_transaction_outputs_forced(self) -> Vec { self.transaction_outputs }