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 daec45ada0b..ff7cb7381c2 100644 Binary files a/aptos-move/framework/cached-packages/src/head.mrb and b/aptos-move/framework/cached-packages/src/head.mrb differ diff --git a/aptos-move/framework/move-stdlib/sources/configs/features.move b/aptos-move/framework/move-stdlib/sources/configs/features.move index 87d769535f2..945d7e7092d 100644 --- a/aptos-move/framework/move-stdlib/sources/configs/features.move +++ b/aptos-move/framework/move-stdlib/sources/configs/features.move @@ -780,6 +780,19 @@ module std::features { is_enabled(ORDERLESS_TRANSACTIONS) } + /// Whether lazy loading is enabled, so that only immediate dependencies are linked and charged + /// at load time rather than the whole transitive closure. + /// Lifetime: transient + const ENABLE_LAZY_LOADING: u64 = 95; + + public fun get_lazy_loading_feature(): u64 { + ENABLE_LAZY_LOADING + } + + public fun is_lazy_loading_enabled(): bool { + is_enabled(ENABLE_LAZY_LOADING) + } + /// Whether to calculate the transaction fee for distribution. const CALCULATE_TRANSACTION_FEE_FOR_DISTRIBUTION: u64 = 96; @@ -959,6 +972,21 @@ module std::features { /// Lifetime: permanent const HOT_STATE_ROOT_IN_TXN_INFO: u64 = 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, so published code becomes active + /// only from the next block. + /// Lifetime: permanent + const DEFERRED_MODULE_PUBLISHING: u64 = 124; + + public fun get_deferred_module_publishing_feature(): u64 { + DEFERRED_MODULE_PUBLISHING + } + + public fun is_deferred_module_publishing_enabled(): bool { + is_enabled(DEFERRED_MODULE_PUBLISHING) + } + // ============================================================================================ // Feature Flag Implementation diff --git a/aptos-move/framework/natives/Cargo.toml b/aptos-move/framework/natives/Cargo.toml index a66ad62128a..46b7dcaf942 100644 --- a/aptos-move/framework/natives/Cargo.toml +++ b/aptos-move/framework/natives/Cargo.toml @@ -42,6 +42,7 @@ bcs = { workspace = true } better_any = { workspace = true } blake2-rfc = { workspace = true } bulletproofs = { workspace = true } +bytes = { workspace = true } byteorder = { workspace = true } curve25519-dalek = { package = "curve25519-dalek-ng", version = "4" } either = { workspace = true } diff --git a/aptos-move/framework/natives/src/code.rs b/aptos-move/framework/natives/src/code.rs index 2f6d13f4a50..d412c35d0cf 100644 --- a/aptos-move/framework/natives/src/code.rs +++ b/aptos-move/framework/natives/src/code.rs @@ -1,30 +1,48 @@ // 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 -use crate::unzip_metadata_str; +use crate::{ + publish_verification::publish::validate_publish_request, + transaction_context::NativeTransactionContext, unzip_metadata_str, +}; use anyhow::bail; -use aptos_gas_schedule::gas_params::natives::aptos_framework::*; +use aptos_gas_schedule::{gas_feature_versions, gas_params::natives::aptos_framework::*}; use aptos_native_interface::{ safely_pop_arg, RawSafeNative, SafeNativeBuilder, SafeNativeContext, SafeNativeError, SafeNativeResult, }; use aptos_types::{ - move_any::Any, on_chain_config::OnChainConfig, transaction::ModuleBundle, vm_status::StatusCode, + chain_id::ChainId, + move_any::Any, + on_chain_config::{FeatureFlag, OnChainConfig, TimedFeatureFlag}, + state_store::state_key::StateKey, + transaction::ModuleBundle, + vm_status::StatusCode, }; use better_any::{Tid, TidAble}; -use move_binary_format::errors::{PartialVMError, PartialVMResult}; +use bytes::Bytes; +use move_binary_format::{ + access::ModuleAccess, + check_complexity::check_module_complexity, + compatibility::Compatibility, + errors::{PartialVMError, PartialVMResult}, + CompiledModule, +}; use move_core_types::{ account_address::AccountAddress, gas_algebra::NumBytes, ident_str, identifier::IdentStr, + language_storage::ModuleId, move_resource::{MoveResource, MoveStructType}, }; use move_vm_runtime::{ native_extensions::{NativeRuntimeRefCheckModelsCompleted, SessionListener}, native_functions::NativeFunction, + ModuleStorage, StagingModuleStorage, WithRuntimeEnvironment, }; use move_vm_types::{ + gas::DependencyKind, loaded_data::runtime_types::Type, values::{Struct, Value}, }; @@ -359,6 +377,236 @@ fn native_request_publish( Ok(smallvec![]) } +/*************************************************************************************************** + * native fun verify_package( + * owner: address, + * expected_modules: vector, + * 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 }