Repository: rooch-network/rooch
Pull request #3473: [cli] Check the modules compatibility when publish
Author: login: jolestar
Created: 2025-03-22T04:54:35Z | Merged: 2025-03-24T16:34:34Z

## Summary

Check the modules compatibility when publish



Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 8131ccbe95..f0660772f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -10344,6 +10344,7 @@ dependencies = [
  "csv",
  "datatest-stable 0.1.1",
  "fastcrypto 0.1.8 (git+https://github.com/MystenLabs/fastcrypto?rev=56f6223b84ada922b6cb2c672c69db2ea3dc6a13)",
+ "framework-builder",
  "framework-release",
  "framework-types",
  "hdrhistogram",
diff --git a/crates/rooch/Cargo.toml b/crates/rooch/Cargo.toml
index cd0cfd397d..c1b19cdc2b 100644
--- a/crates/rooch/Cargo.toml
+++ b/crates/rooch/Cargo.toml
@@ -109,6 +109,7 @@ rooch-faucet = { workspace = true }
 rooch-oracle = { workspace = true }
 
 framework-release = { workspace = true }
+framework-builder = { workspace = true }
 
 #We should keep the allocator in the last of the dependencies
 [target.'cfg(not(target_env = "msvc"))'.dependencies]
diff --git a/crates/rooch/src/commands/move_cli/commands/publish.rs b/crates/rooch/src/commands/move_cli/commands/publish.rs
index 83b29e4466..481b2a1961 100644
--- a/crates/rooch/src/commands/move_cli/commands/publish.rs
+++ b/crates/rooch/src/commands/move_cli/commands/publish.rs
@@ -5,6 +5,8 @@ use crate::cli_types::{CommandAction, TransactionOptions, WalletContextOptions};
 use crate::tx_runner::dry_run_tx_locally;
 use async_trait::async_trait;
 use clap::Parser;
+use framework_builder::releaser;
+use move_binary_format::CompiledModule;
 use move_cli::Move;
 use move_core_types::account_address::AccountAddress;
 use move_core_types::effects::Op;
@@ -85,6 +87,23 @@ impl MemoryModuleResolver {
 
         Ok(())
     }
+
+    pub fn get_modules(
+        &self,
+        package_addr: &AccountAddress,
+    ) -> Result<Vec<CompiledModule>, anyhow::Error> {
+        let modules = self.packages.get(package_addr);
+        if modules.is_none() {
+            return Ok(vec![]);
+        }
+        let modules = modules.unwrap();
+        let mut compiled_modules = vec![];
+        for module_bytes in modules.values() {
+            let compiled_module = CompiledModule::deserialize(module_bytes)?;
+            compiled_modules.push(compiled_module);
+        }
+        Ok(compiled_modules)
+    }
 }
 
 impl ModuleResolver for MemoryModuleResolver {
@@ -150,6 +169,10 @@ pub struct Publish {
     /// Run the DryRun for this transaction
     #[clap(long, default_value = "false")]
     dry_run: bool,
+
+    /// Skip the client side compatibility check
+    #[clap(long, default_value = "false")]
+    skip_client_compat_check: bool,
 }
 
 #[async_trait]
@@ -214,12 +237,7 @@ impl CommandAction<ExecuteTransactionResponseView> for Publish {
             .map(|(mid, _)| mid.clone())
             .collect::<Vec<_>>();
 
-        //Because the verify modules function will load many modules from the rpc server,
-        //We need to download all modules in one rpc request and then verify the modules.
-        let mut resolver = MemoryModuleResolver::new(context.get_client().await?);
-        resolver.download(all_module_ids)?;
-        moveos_verifier::verifier::verify_modules(&sorted_modules, &resolver)?;
-        for module in sorted_modules {
+        for module in &sorted_modules {
             let module_address = module.self_id().address().to_owned();
             if module_address != pkg_address {
                 return Err(RoochError::MoveCompilationError(format!(
@@ -233,8 +251,18 @@ impl CommandAction<ExecuteTransactionResponseView> for Publish {
             bundles.push(binary);
         }
 
+        //Because the verify modules function will load many modules from the rpc server,
+        //We need to download all modules in one rpc request and then verify the modules.
+        let mut resolver = MemoryModuleResolver::new(context.get_client().await?);
+        resolver.download(all_module_ids)?;
+        moveos_verifier::verifier::verify_modules(&sorted_modules, &resolver)?;
+        let old_modules = resolver.get_modules(&pkg_address)?;
+        if !old_modules.is_empty() && !self.skip_client_compat_check {
+            releaser::check_modules_compat(sorted_modules, old_modules, true)?;
+        }
+
         // Create a sender RoochAddress
-        eprintln!("Publish modules to address: {:?}", pkg_address);
+        eprintln!("Publish modules to address: {}", pkg_address);
 
         // Prepare and execute the transaction based on the action type
         let tx_result = if !self.by_move_action {
diff --git a/crates/rooch/src/tx_runner.rs b/crates/rooch/src/tx_runner.rs
index 3265ce4ee7..ece918759d 100644
--- a/crates/rooch/src/tx_runner.rs
+++ b/crates/rooch/src/tx_runner.rs
@@ -52,11 +52,7 @@ pub fn execute_tx_locally(
     let (move_mv, object_runtime, client_resolver, action, cost_table) =
         prepare_execute_env(root_object_meta, &client_resolver, tx.clone());
 
-    let mut gas_meter = MoveOSGasMeter::new(
-        cost_table,
-        GasScheduleConfig::CLI_DEFAULT_MAX_GAS_AMOUNT,
-        true,
-    );
+    let mut gas_meter = MoveOSGasMeter::new(cost_table, tx.max_gas_amount, true);
 
     // The dry run supports unsigned transactions, but when calculating the transaction size,
     // the length of the signature part needs to be included.
diff --git a/crates/testsuite/features/cmd.feature b/crates/testsuite/features/cmd.feature
index d08e1ccbb8..56bead78d9 100644
--- a/crates/testsuite/features/cmd.feature
+++ b/crates/testsuite/features/cmd.feature
@@ -304,7 +304,7 @@ Feature: Rooch CLI integration tests
       Then assert: "{{$.move[-1].execution_info.status.type}} == executed"
 
       # check compatibility
-      Then cmd: "move publish -p ../../examples/entry_function_arguments_old/  --named-addresses rooch_examples=default --json"
+      Then cmd: "move publish -p ../../examples/entry_function_arguments_old/  --named-addresses rooch_examples=default --skip-client-compat-check --json"
       Then assert: "'{{$.move[-1].execution_info.status.type}}' == 'moveabort'"
 
       Then stop the server
diff --git a/crates/testsuite/features/friend_module_renaming.feature b/crates/testsuite/features/friend_module_renaming.feature
index 6b5496ea95..646fb56c95 100644
--- a/crates/testsuite/features/friend_module_renaming.feature
+++ b/crates/testsuite/features/friend_module_renaming.feature
@@ -8,5 +8,5 @@ Feature: Rooch CLI rename module friend list
         Then cmd: "move publish -p ../../examples/rename_friend_module  --named-addresses rooch_examples=default --json"
         Then assert: "{{$.move[-1].execution_info.status.type}} == executed"
 
-        Then cmd: "move publish -p ../../examples/rename_friend_module_trigger  --named-addresses rooch_examples=default --json"
+        Then cmd: "move publish -p ../../examples/rename_friend_module_trigger  --named-addresses rooch_examples=default --skip-client-compat-check --json"
         Then assert: "{{$.move[-1].execution_info.status.type}} == moveabort"
diff --git a/crates/testsuite/tests/integration.rs b/crates/testsuite/tests/integration.rs
index 25e3ee3fa6..9eb5eceac8 100644
--- a/crates/testsuite/tests/integration.rs
+++ b/crates/testsuite/tests/integration.rs
@@ -61,7 +61,6 @@ impl Default for World {
 #[given(expr = "a server for {word}")] // Cucumber Expression
 async fn start_server(w: &mut World, _scenario: String) {
     tokio::time::sleep(Duration::from_secs(5)).await;
-
     let mut service = Service::new();
     wait_port_available(w.opt.port()).await;
 
diff --git a/frameworks/framework-builder/src/releaser.rs b/frameworks/framework-builder/src/releaser.rs
index f1fee6f099..6141a5a6a8 100644
--- a/frameworks/framework-builder/src/releaser.rs
+++ b/frameworks/framework-builder/src/releaser.rs
@@ -9,6 +9,7 @@ use anyhow::{bail, ensure, Result};
 use framework_types::addresses::ROOCH_NURSERY_ADDRESS;
 use itertools::Itertools;
 use move_binary_format::{compatibility::Compatibility, errors::PartialVMResult, CompiledModule};
+use moveos_types::moveos_std::module_store::PackageData;
 use std::collections::HashMap;
 use tracing::{debug, info, warn};
 
@@ -111,15 +112,23 @@ fn current_max_version() -> u64 {
 
 /// Check whether the new stdlib is compatible with the old stdlib
 fn check_stdlib_compatibility(curr_stdlib: &Stdlib, prev_stdlib: &Stdlib) -> Result<()> {
-    let new_modules_map = curr_stdlib
-        .all_modules()
-        .expect("Extract modules from new stdlib failed")
+    check_modules_compat(
+        curr_stdlib.all_modules()?,
+        prev_stdlib.all_modules()?,
+        false,
+    )
+}
+
+pub fn check_modules_compat(
+    new_modules: Vec<CompiledModule>,
+    old_modules: Vec<CompiledModule>,
+    allow_deleted_module: bool,
+) -> Result<()> {
+    let new_modules_map = new_modules
         .into_iter()
         .map(|module| (module.self_id(), module))
         .collect::<HashMap<_, _>>();
-    let old_modules_map = prev_stdlib
-        .all_modules()
-        .expect("Extract modules from old stdlib failed")
+    let old_modules_map = old_modules
         .into_iter()
         .map(|module| (module.self_id(), module))
         .collect::<HashMap<_, _>>();
@@ -174,17 +183,36 @@ fn check_stdlib_compatibility(curr_stdlib: &Stdlib, prev_stdlib: &Stdlib) -> Res
         })
         .collect::<Vec<_>>();
 
-    ensure!(
-        deleted_module_ids.is_empty(),
-        "Modules {} is deleted!",
-        deleted_module_ids
-            .into_iter()
-            .map(|module_id| module_id.to_string())
-            .join(",")
-    );
+    if !allow_deleted_module {
+        ensure!(
+            deleted_module_ids.is_empty(),
+            "Modules {} is deleted!",
+            deleted_module_ids
+                .into_iter()
+                .map(|module_id| module_id.to_string())
+                .join(",")
+        );
+    } else {
+        warn!(
+            "Modules {} is deleted in local, but them still on the Chain!. If you want to deprecated them, please abort the all public functions in the module.",
+            deleted_module_ids
+                .into_iter()
+                .map(|module_id| module_id.to_string())
+                .join(",")
+        );
+    }
     Ok(())
 }
 
+pub fn check_package_compat(
+    new_package_data: PackageData,
+    pre_package_data: PackageData,
+) -> Result<()> {
+    let new_modules = new_package_data.compiled_modules()?;
+    let pre_modules = pre_package_data.compiled_modules()?;
+    check_modules_compat(new_modules, pre_modules, false)
+}
+
 /// check module compatibility
 fn check_compiled_module_compat(
     old_module: &CompiledModule,
@@ -200,6 +228,6 @@ fn check_compiled_module_compat(
     );
     // TODO: config compatibility through global configuration
     // We allow `friend` function to be broken
-    let compat = Compatibility::new(true, true, false);
+    let compat = Compatibility::new(true, true, true);
     compat.check(old_module, new_module)
 }
diff --git a/moveos/moveos-types/src/moveos_std/module_store.rs b/moveos/moveos-types/src/moveos_std/module_store.rs
index 0ef32af4f3..17f96e37f2 100644
--- a/moveos/moveos-types/src/moveos_std/module_store.rs
+++ b/moveos/moveos-types/src/moveos_std/module_store.rs
@@ -10,6 +10,7 @@ use crate::{
     state::{MoveStructState, MoveStructType},
 };
 use anyhow::Result;
+use move_binary_format::CompiledModule;
 use move_core_types::language_storage::StructTag;
 use move_core_types::{
     account_address::AccountAddress,
@@ -128,6 +129,13 @@ impl PackageData {
         file.write_all(&contents)?;
         Ok(())
     }
+
+    pub fn compiled_modules(&self) -> Result<Vec<CompiledModule>> {
+        self.modules
+            .iter()
+            .map(|bytes| CompiledModule::deserialize(bytes).map_err(Into::into))
+            .collect()
+    }
 }
 
 impl MoveStructType for PackageData {


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR enhances module compatibility checks during the package publish process while refactoring the compatibility functions and updating gas metering. Key changes include:  
- Integrating new module fetching and compatibility verification in the publish command.  
- Refactoring compatibility checks in framework-builder with a new check_package_compat function.  
- Adding a helper function for deserializing compiled modules in PackageData and updating gas meter initialization in tx_runner.

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| crates/rooch/src/commands/move_cli/commands/publish.rs | Updated module download and verification by integrating get_modules and check_modules_compat. |
| frameworks/framework-builder/src/releaser.rs | Refactored compatibility functions, adjusted compatibility flags, and introduced check_package_compat. |
| moveos/moveos-types/src/moveos_std/module_store.rs | Added compiled_modules helper to deserialize modules from PackageData. |
| crates/rooch/Cargo.toml | Added framework-builder dependency. |
| crates/rooch/src/tx_runner.rs | Updated gas meter initialization to use tx.max_gas_amount. |
</details>



<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**frameworks/framework-builder/src/releaser.rs:227**
* Verify that enabling friend function compatibility (the third parameter set to true) is intended, as it may allow breaking changes if friend functions change.
```
let compat = Compatibility::new(true, true, true);
```
**crates/rooch/src/tx_runner.rs:55**
* Ensure that using tx.max_gas_amount instead of the CLI default gas constant does not introduce unexpected behavior in gas metering.
```
let mut gas_meter = MoveOSGasMeter::new(cost_table, tx.max_gas_amount, true);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-22T04:55:43Z
user: login: steelgeek091
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T06:37:31Z

Review comments:
diff_hunk: @@ -111,15 +112,19 @@ fn current_max_version() -> u64 {
 
 /// Check whether the new stdlib is compatible with the old stdlib
 fn check_stdlib_compatibility(curr_stdlib: &Stdlib, prev_stdlib: &Stdlib) -> Result<()> {
-    let new_modules_map = curr_stdlib
-        .all_modules()
-        .expect("Extract modules from new stdlib failed")
+    check_modules_compat(curr_stdlib.all_modules()?, prev_stdlib.all_modules()?)
path: frameworks/framework-builder/src/releaser.rs
user: login: Copilot
body: The call to check_modules_compat is missing the required third parameter 'allow_deleted_module'. Please update the call to supply a boolean value.
```suggestion
    check_modules_compat(curr_stdlib.all_modules()?, prev_stdlib.all_modules()?, false)
```
created_at: 2025-03-22T04:55:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,17 +179,36 @@ fn check_stdlib_compatibility(curr_stdlib: &Stdlib, prev_stdlib: &Stdlib) -> Res
         })
         .collect::<Vec<_>>();
 
-    ensure!(
-        deleted_module_ids.is_empty(),
-        "Modules {} is deleted!",
-        deleted_module_ids
-            .into_iter()
-            .map(|module_id| module_id.to_string())
-            .join(",")
-    );
+    if !allow_deleted_module {
+        ensure!(
+            deleted_module_ids.is_empty(),
+            "Modules {} is deleted!",
+            deleted_module_ids
+                .into_iter()
+                .map(|module_id| module_id.to_string())
+                .join(",")
+        );
+    } else {
+        warn!(
+            "Modules {} is deleted in local, but them still on the Chain!. If you want to deprecated them, please abort the all public functions in the module.",
+            deleted_module_ids
+                .into_iter()
+                .map(|module_id| module_id.to_string())
+                .join(",")
+        );
+    }
     Ok(())
 }
 
+pub fn check_package_compat(
+    new_package_data: PackageData,
+    pre_package_data: PackageData,
+) -> Result<()> {
+    let new_modules = new_package_data.compiled_modules()?;
+    let pre_modules = pre_package_data.compiled_modules()?;
+    check_modules_compat(new_modules, pre_modules)
path: frameworks/framework-builder/src/releaser.rs
user: login: Copilot
body: check_modules_compat requires three arguments; please provide the missing 'allow_deleted_module' boolean parameter in this call within check_package_compat.
```suggestion
    check_modules_compat(new_modules, pre_modules, false)
```
created_at: 2025-03-22T04:55:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-22T04:54:39Z
body: [vc]: #EAut0ud0Mv9LGm9cixRObO97i4hQY1wuALLX+IResVA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJyb29jaCIsInJvb3REaXJlY3RvcnkiOiJkb2NzL3dlYnNpdGUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vcm9vY2gvcm9vY2gvQXhNc29MOU5KcmE4ZW5iSnJ2VDQ1OHhka3RYUyIsInByZXZpZXdVcmwiOiJyb29jaC1naXQtY29tcGF0Y2hlY2t3aGVucHVibGlzaC1yb29jaC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19LHsibmFtZSI6InJvb2NoLXBvcnRhbC12Mi4xIiwicm9vdERpcmVjdG9yeSI6ImluZnJhL3Jvb2NoLXBvcnRhbC12MiIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9yb29jaC9yb29jaC1wb3J0YWwtdjIuMS9GWEJDUGc3MjFRb2syNUV1c1N0d2d1VFdybXdqIiwicHJldmlld1VybCI6InJvb2NoLXBvcnRhbC12MjEtZ2l0LWNvbXBhdGNoZWNrd2hlbnB1Ymxpc2gtcm9vY2gudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJyb29jaC1wb3J0YWwtdjIxLWdpdC1jb21wYXRjaGVja3doZW5wdWJsaXNoLXJvb2NoLnZlcmNlbC5hcHAifX0seyJuYW1lIjoidGVzdC1wb3J0YWwiLCJyb290RGlyZWN0b3J5IjoiaW5mcmEvcm9vY2gtcG9ydGFsLXYyIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Jvb2NoL3Rlc3QtcG9ydGFsL0ZMWExkRktDZ0hXaHRDRzRlZGM3NUxXeDdXWW4iLCJwcmV2aWV3VXJsIjoidGVzdC1wb3J0YWwtZ2l0LWNvbXBhdGNoZWNrd2hlbnB1Ymxpc2gtcm9vY2gudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ0ZXN0LXBvcnRhbC1naXQtY29tcGF0Y2hlY2t3aGVucHVibGlzaC1yb29jaC52ZXJjZWwuYXBwIn19LHsibmFtZSI6Im9uY2hhaW4tYWktY2hhdCIsInJvb3REaXJlY3RvcnkiOiJleGFtcGxlcy9vbmNoYWluX2FpX2NoYXQvd2ViIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Jvb2NoL29uY2hhaW4tYWktY2hhdC82SEw5UHVHa1VGanVwUDFWOFZrcFlwTHRqdjV3IiwicHJldmlld1VybCI6Im9uY2hhaW4tYWktY2hhdC1naXQtY29tcGF0Y2hlY2t3aGVucHVibGlzaC1yb29jaC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6Im9uY2hhaW4tYWktY2hhdC1naXQtY29tcGF0Y2hlY2t3aGVucHVibGlzaC1yb29jaC52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **onchain-ai-chat** | ✅ Ready ([Inspect](https://vercel.com/rooch/onchain-ai-chat/6HL9PuGkUFjupP1V8VkpYpLtjv5w)) | [Visit Preview](https://onchain-ai-chat-git-compatcheckwhenpublish-rooch.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/onchain-ai-chat-git-compatcheckwhenpublish-rooch.vercel.app?via=pr-comment-feedback-link) | Mar 24, 2025 1:17am |
| **rooch-portal-v2.1** | ✅ Ready ([Inspect](https://vercel.com/rooch/rooch-portal-v2.1/FXBCPg721Qok25EusStwguTWrmwj)) | [Visit Preview](https://rooch-portal-v21-git-compatcheckwhenpublish-rooch.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/rooch-portal-v21-git-compatcheckwhenpublish-rooch.vercel.app?via=pr-comment-feedback-link) | Mar 24, 2025 1:17am |
| **test-portal** | ✅ Ready ([Inspect](https://vercel.com/rooch/test-portal/FLXLdFKCgHWhtCG4edc75LWx7WYn)) | [Visit Preview](https://test-portal-git-compatcheckwhenpublish-rooch.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/test-portal-git-compatcheckwhenpublish-rooch.vercel.app?via=pr-comment-feedback-link) | Mar 24, 2025 1:17am |

<details><summary>1 Skipped Deployment</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **rooch** | ⬜️ Ignored ([Inspect](https://vercel.com/rooch/rooch/AxMsoL9NJra8enbJrvT458xdktXS)) | [Visit Preview](https://rooch-git-compatcheckwhenpublish-rooch.vercel.app) |  | Mar 24, 2025 1:17am |
</details>





author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-22T04:54:56Z
body: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>OpenSSF Scorecard</h2>
<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>
</table><h2>Scanned Files</h2>
<ul>


<!-- dependency-review-pr-comment-marker -->
author_association: NONE