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