dApp Docs/AI Agent 合约升级与迁移模式指南
Development reference. Not independently verified for production.

MSG Chain AI Agent 合约升级与迁移模式指南

适用对象

本指南面向需要在 MSG Chain(msg-chain-1)上管理 CosmWasm 智能合约生命周期升级的 AI Agent 开发者、合约工程师与基础设施运维团队。

⚠️ No-Go Disclaimer: MSGChain 主网裁决为 No-Go。本文件所有内容反映的是开发阶段的技术设计,不代表主网未独立核验上线状态。生产部署状态请以白皮书为准:https://msgchain.org/whitepaper/


目录

  1. 引言
  2. CosmWasm 升级机制
  3. 迁移生命周期
  4. 数据迁移策略
  5. 代理模式(Proxy Pattern)
  6. 自动迁移治理
  7. 回滚与应急回退
  8. 升级测试
  9. AI Agent 升级工作流
  10. 实战:AI Agent 合约版本演进案例
  11. 安全注意事项
  12. 总结

1. 引言

1.1 不可变性 vs 可升级性

智能合约部署到区块链后,其字节码在链上不可篡改。这是区块链信任的基础——用户能够验证他们交互的代码与审计过的代码完全一致。然而,这种不可变性也给软件演进带来了根本性矛盾:

CosmWasm 通过 migrate 入口点提供了受控的可升级机制,在不牺牲不可变性的前提下,允许合约从一个 code_id 迁移到另一个 code_id。这是 Cosmos 生态相较于以太坊 Solidity 合约的一个关键优势——后者通常需要依赖代理模式或不可变合约配合新部署来实现升级。

1.2 AI Agent 的演进需求

在 MSG Chain 上运行的 AI Agent 合约面临比传统 DeFi 合约更频繁的演进需求:

这些需求使得合约升级成为 AI Agent 生命周期管理的核心能力。

1.3 MSG Chain 升级基础能力

MSG Chain(msg-chain-1)的 CosmWasm 运行时提供以下升级基础设施:

本指南将深入探讨这些机制的工作原理、最佳实践以及在 AI Agent 场景下的具体应用。


2. CosmWasm 升级机制

2.1 Migrate Entry Point

CosmWasm 合约的生命周期包含五个入口点(entry point),其中 migrate 是升级的核心:

instantiate  →  执行    →  query
                  ↓
              sudo
                  ↓
              migrate
                  ↓
              reply

migrate 入口点的签名如下:

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(
    deps: DepsMut,
    env: Env,
    msg: MigrateMsg,
) -> Result<Response, ContractError> {
    // 验证迁移权限
    // 检查 CW2 版本兼容性
    // 执行状态迁移
    // 更新 CW2 版本信息
}

与 instantiate 不同,migrate 接收一个已初始化的存储(已有状态)。它必须:

  1. 验证调用者是否有权执行迁移
  2. 从旧状态中读取数据
  3. 根据 MigrateMsg 中的指令执行数据转换
  4. 将转换后的数据写入新存储结构
  5. 更新 CW2 合约版本信息

2.2 MigrateMsg 设计

MigrateMsg 是迁移的输入参数,由新合约定义。它应当包含迁移所需的所有配置指令:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MigrateMsg {
    /// 迁移版本标识,用于版本兼容性检查
    pub version: String,
    /// 是否执行数据迁移(true)或仅更新 code_id(false)
    pub migrate_state: bool,
    /// 新合约的配置参数(如果配置结构发生了变化)
    pub config: Option<Config>,
    /// 迁移后的管理员地址(None 表示不变)
    pub new_admin: Option<String>,
    /// 迁移钩子参数,传递给特定迁移逻辑
    pub hook_params: Option<Binary>,
}

设计原则:

2.3 CW2 版本追踪规范

CW2(cw2)是 CosmWasm 官方规范,用于在合约存储中记录合约的标识和版本信息。它被存储在固定的存储 key 下,确保即使合约代码变更也能读取。

pub struct ContractVersion {
    /// 合约名称,用于标识合约逻辑族
    pub contract: String,
    /// 语义化版本号
    pub version: String,
}

存储位置:

pub const CONTRACT_VERSION: Item<ContractVersion> = Item::new("contract_info");

在 instantiate 中设置:

use cw2::set_contract_version;

const CONTRACT_NAME: &str = "crates.io:my-ai-agent";
const CONTRACT_VERSION: &str = "1.0.0";

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(...) -> Result<Response, ContractError> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
}

在 migrate 中更新:

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let ver = cw2::get_contract_version(deps.storage)?;

    if ver.contract != CONTRACT_NAME {
        return Err(ContractError::ContractNameMismatch {
            expected: CONTRACT_NAME.to_string(),
            found: ver.contract,
        });
    }

    match ver.version.as_str() {
        "1.0.0" => migrate_v1_to_v2(deps, env, &msg)?,
        "1.1.0" => migrate_v1_1_to_v2(deps, env, &msg)?,
        v => return Err(ContractError::UnsupportedMigration {
            from: v.to_string(),
            to: CONTRACT_VERSION.to_string(),
        }),
    }

    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    Ok(Response::new().add_attribute("migrated_from", ver.version))
}

2.4 MSG Chain 的版本追踪增强

在 MSG Chain 生态中,CW2 信息通过 genesis_registry_v1 在注册中心层面做了一层增强映射。注册中心记录每个 canonical key 对应的:

这种双层记录(合约内 CW2 + 注册中心映射)提供了更可靠的版本溯源能力:

pub struct RegistryEntry {
    pub canonical_key: String,
    pub contract_address: String,
    pub current_code_id: u64,
    pub cw2_contract: String,
    pub cw2_version: String,
    pub migration_log: Vec<MigrationRecord>,
}

pub struct MigrationRecord {
    pub from_code_id: u64,
    pub to_code_id: u64,
    pub block_height: u64,
    pub tx_hash: String,
    pub migrator: String,
}

2.5 StoreCode → Instantiate → Migrate 流程关系

在 CosmWasm 中,合约的完整生命周期涉及三个核心交易类型:

交易 作用 升级相关
StoreCode 上传 wasm 字节码到链上,获得 code_id 新版本合约代码必须先 StoreCode
Instantiate 基于 code_id 创建合约实例 新合约首次部署时使用
Migrate 将已有合约实例切换到新 code_id 升级操作的核心

三者关系:

StoreCode (code_id = 2)
    ↓
Instantiate (code_id = 2) → 新合约实例(首次部署)
    ↓
... 运行时间 ...
    ↓
StoreCode (code_id = 3)  ← 新版本代码
    ↓
Migrate (addr = old_addr, code_id = 3) ← 升级
    ↓
新逻辑生效,旧状态被迁移

关键区别:Migrate 保留原合约的地址和状态存储,只替换执行逻辑。这意味着与合约地址绑定的资产、权限和引用关系在升级后仍然有效。


3. 迁移生命周期

3.1 完整迁移流程

一个完整的合约迁移在 MSG Chain 上包含以下步骤:

步骤 1:准备新版本合约代码

RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown --locked
sha256sum target/wasm32-unknown-unknown/release/ai_agent.wasm

步骤 2:StoreCode 上传新代码

msg-chaind tx wasm store artifacts/ai_agent_v2.wasm \
  --from deployer \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --gas-adjustment 1.2 \
  -y --output json

StoreCode 成功后获得新的 code_id(例如 code_id = 5)。

步骤 3:验证新代码

msg-chaind query wasm code-info 5 --node https://rpc.msgchain.org
msg-chaind query wasm code 5 --node https://rpc.msgchain.org > ai_agent_v2_downloaded.wasm
sha256sum ai_agent_v2_downloaded.wasm

步骤 4:构建迁移消息

{
  "version": "2.0.0",
  "migrate_state": true,
  "config": {
    "max_gas_per_task": 500000,
    "cooldown_period": 3600,
    "fee_bps": 50
  },
  "hook_params": null
}

步骤 5:执行 Migrate 交易

MSG_CHAIN_RPC="https://rpc.msgchain.org"
CONTRACT_ADDR="msg1contract_address..."

msg-chaind query wasm contract $CONTRACT_ADDR \
  --node $MSG_CHAIN_RPC -o json | jq '.contract_info.code_id'

msg-chaind tx wasm migrate $CONTRACT_ADDR 5 \
  "$(cat migrate_msg_v2.json)" \
  --from multisig \
  --chain-id msg-chain-1 \
  --node $MSG_CHAIN_RPC \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --gas-adjustment 1.2 \
  -y --output json

步骤 6:验证迁移结果

msg-chaind query wasm contract $CONTRACT_ADDR \
  --node $MSG_CHAIN_RPC -o json | jq '.contract_info.code_id'

msg-chaind query wasm contract-state smart $CONTRACT_ADDR \
  '{"config":{}}' \
  --node $MSG_CHAIN_RPC

msg-chaind query wasm contract-state raw $CONTRACT_ADDR \
  636F6E74726163745F696E666F \
  --node $MSG_CHAIN_RPC -o json | jq '.data' -r | base64 -d 2>/dev/null || true

步骤 7:更新注册中心

如果合约在 genesis_registry_v1 中注册,需要在迁移后更新注册中心的 code_id 映射:

pub struct UpdateContractMsg {
    pub canonical_key: String,
    pub new_code_id: u64,
    pub new_code_hash: String,
    pub migrator_signature: Binary,
}

3.2 与 MSG Chain Registry 的代码码注册

MSG Chain 的 genesis_registry_v1 合约维护了一个从 canonical key 到合约地址/代码 ID 的映射。

注册中心的升级相关查询:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RegistryQueryMsg {
    Resolve { canonical_key: String },
    ReverseResolve { contract_address: String },
    MigrationHistory { canonical_key: String },
    List { start_after: Option<String>, limit: Option<u32> },
}

注册中心的升级相关执行:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RegistryExecuteMsg {
    Register {
        canonical_key: String,
        contract_address: String,
        code_id: u64,
    },
    UpdateCodeId {
        canonical_key: String,
        new_code_id: u64,
    },
    Deprecate {
        canonical_key: String,
        migration_target: Option<String>,
    },
}

迁移后的注册更新:

msg-chaind tx wasm execute $REGISTRY_ADDR \
  '{"update_code_id":{"canonical_key":"ai_agent_v1","new_code_id":5}}' \
  --from multisig \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org \
  --gas auto --gas-prices 1000000000attoMSG -y

3.3 AI Agent 升级的批准门禁

根据 MSG Chain 的交付工作流,合约迁移操作必须经过以下门禁:

secret_injection 门禁
  └── 需要:签名账户、Gas 预算确认
        ↓
production_release 门禁
  └── 需要:最终审批、回滚确认、资金与治理风险确认
        ↓
governance_or_treasury 门禁
  └── 需要:多签签名阈值达成(主网 2_of_3)
        ↓
执行 migrate 交易

AI Agent 在执行迁移前必须检查这些门禁的状态,不得越过审批门禁自主执行迁移。

3.4 迁移状态机

合约在其生命周期中可能经历多次迁移。建议合约内部维护一个 MigrationState:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum MigrationState {
    Fresh,
    MigratingInProgress,
    Migrated { from_version: String, to_version: String, at_height: u64 },
    RolledBack { from_version: String, to_version: String, at_height: u64 },
    Deprecated { migration_target: Option<String> },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
    pub owner: Addr,
    pub migration_state: MigrationState,
    pub migration_history: Vec<MigrationRecord>,
}

4. 数据迁移策略

4.1 状态迁移的挑战

与传统的数据库迁移不同,链上状态迁移面临以下约束:

4.2 状态迁移模式

4.2.1 直接迁移(Inline Migration)

最简单的方式:在 migrate 入口点中直接读取旧状态,转换后写入新存储。

pub fn migrate_v1_to_v2(deps: DepsMut, env: Env, msg: &MigrateMsg) -> Result<(), ContractError> {
    let old_state: OldState = OLD_STATE_KEY.load(deps.storage)?;

    let new_state = StateV2 {
        owner: old_state.owner,
        config: msg.config.clone().unwrap_or_default(),
        total_tasks: old_state.total_tasks,
        completed_tasks: old_state.completed_tasks,
        metadata: HashMap::new(),
        created_at: old_state.created_at,
        updated_at: env.block.time,
    };

    STATE.save(deps.storage, &new_state)?;
    OLD_STATE_KEY.remove(deps.storage);

    let old_tasks: Vec<OldTask> = OLD_TASKS.load(deps.storage)?;
    for old_task in old_tasks {
        let new_task = TaskV2 {
            id: old_task.id,
            description: old_task.description,
            status: match old_task.completed {
                true => TaskStatus::Completed,
                false => TaskStatus::Pending,
            },
            assignee: old_task.assignee,
            created_at: old_task.created_at,
            completed_at: old_task.completed_at,
            tags: vec![],
            priority: 0,
        };
        TASKS.save(deps.storage, old_task.id, &new_task)?;
    }
    OLD_TASKS.remove(deps.storage);

    Ok(())
}

适用场景:数据量小、转换逻辑简单、单次 Gas 内可完成。

4.2.2 懒迁移(Lazy Migration)

对于大型数据集,在 migrate 中一次性转换所有数据可能不现实。懒迁移策略只迁移元数据,用户数据在首次访问时按需转换。

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VersionedState {
    pub data_version: String,
    pub config: Config,
    pub needs_migration: bool,
}

pub fn migrate_v1_to_v2_lazy(deps: DepsMut, env: Env, msg: &MigrateMsg) -> Result<(), ContractError> {
    let old_config: OldConfig = OLD_CONFIG.load(deps.storage)?;

    let state = VersionedState {
        data_version: "2.0.0".to_string(),
        config: Config {
            owner: deps.api.addr_validate(&old_config.owner)?,
            fee_bps: msg.config.as_ref().map(|c| c.fee_bps).unwrap_or(old_config.fee_bps),
            cooldown: msg.config.as_ref().map(|c| c.cooldown).unwrap_or(old_config.cooldown),
        },
        needs_migration: true,
    };
    STATE.save(deps.storage, &state)?;

    Ok(())
}

提供批理触发消息:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    MigrateOldData {
        start_after: Option<u64>,
        limit: u32,
    },
}

pub fn execute_migrate_old_data(
    deps: DepsMut,
    env: Env,
    start_after: Option<u64>,
    limit: u32,
) -> Result<Response, ContractError> {
    let state = STATE.load(deps.storage)?;
    if !state.needs_migration {
        return Err(ContractError::NoMigrationNeeded {});
    }

    let mut converted = 0u64;
    let old_tasks: Vec<(u64, OldTask)> = OLD_TASK.range(
        deps.storage,
        start_after.map(Bound::exclusive),
        None,
        Order::Ascending,
    )
    .take(limit as usize)
    .collect::<Result<Vec<_>, _>>()?;

    for (id, old_task) in &old_tasks {
        let new_task = TaskV2::from(old_task);
        TASKS.save(deps.storage, *id, &new_task)?;
        OLD_TASK.remove(deps.storage, *id);
        converted += 1;
    }

    let remaining = OLD_TASK.keys(deps.storage, None, None, Order::Ascending).count();
    if remaining == 0 {
        STATE.update(deps.storage, |mut s| {
            s.needs_migration = false;
            Ok(s)
        })?;
    }

    Ok(Response::new()
        .add_attribute("action", "migrate_old_data")
        .add_attribute("converted", converted.to_string())
        .add_attribute("remaining", remaining.to_string()))
}

4.2.3 版本化存储(Versioned State)

更系统的方法:在存储层级引入版本号,新旧格式可以共存。

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(tag = "version", content = "data")]
pub enum VersionedTask {
    #[serde(rename = "1")]
    V1 { id: u64, description: String, completed: bool, assignee: String },
    #[serde(rename = "2")]
    V2 { id: u64, description: String, status: String, assignee: String, tags: Vec<String>, priority: u8 },
}

pub const TASKS: Map<u64, VersionedTask> = Map::new("tasks_v");

pub fn read_task(storage: &dyn Storage, task_id: u64) -> Result<Option<TaskV2>, ContractError> {
    match TASKS.may_load(storage, task_id)? {
        Some(VersionedTask::V1(v1)) => Ok(Some(TaskV2 {
            id: v1.id,
            description: v1.description,
            status: if v1.completed { "completed".to_string() } else { "pending".to_string() },
            assignee: v1.assignee,
            tags: vec![],
            priority: 0,
        })),
        Some(VersionedTask::V2(v2)) => Ok(Some(v2)),
        None => Ok(None),
    }
}

4.3 向前兼容模式

在升级时,保持查询接口的向前兼容性可以减少对下游的影响:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    #[serde(rename = "get_task")]
    GetTaskV1 { task_id: u64 },
    GetTaskV2 { task_id: u64 },
    GetTask { task_id: u64, format: Option<String> },
}

pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetTaskV1 { task_id } => {
            let task = query_task_internal(deps, task_id)?;
            to_binary(&TaskV1Response {
                id: task.id, description: task.description,
                completed: task.status == "completed",
                assignee: task.assignee,
            })
        }
        QueryMsg::GetTaskV2 { task_id } => {
            let task = query_task_internal(deps, task_id)?;
            to_binary(&task)
        }
        QueryMsg::GetTask { task_id, format } => {
            let task = query_task_internal(deps, task_id)?;
            match format.as_deref() {
                Some("v1") => to_binary(&TaskV1Response::from(task)),
                _ => to_binary(&task),
            }
        }
    }
}

在 AI Agent 场景中,建议至少保持一个版本的向后兼容窗口。

4.4 存储前缀隔离

不同版本的数据应使用不同的存储前缀:

// v1 存储
pub const OLD_STATE_KEY: Item<OldState> = Item::new("state_v1");
pub const OLD_TASKS: Map<u64, OldTask> = Map::new("tasks_v1");

// v2 存储(不同前缀)
pub const STATE: Item<State> = Item::new("state_v2");
pub const TASKS: Map<u64, TaskV2> = Map::new("tasks_v2");

优点:v1 和 v2 数据可以共存,迁移出错时旧数据仍在旧前缀下。

4.5 迁移失败后的数据恢复

由于 CosmWasm 的原子回滚特性,如果 migrate 返回错误,整个状态修改被撤销,旧状态自动恢复。

pub fn migrate_v1_to_v2(deps: DepsMut, env: Env, msg: &MigrateMsg) -> Result<Response, ContractError> {
    let old_state = OLD_STATE_KEY.load(deps.storage)?;

    let new_state = StateV2::from(old_state.clone());
    STATE_V2.save(deps.storage, &new_state)?;

    let written = STATE_V2.load(deps.storage)?;
    if written.hash() != new_state.hash() {
        return Err(ContractError::MigrationVerificationFailed {});
    }

    OLD_STATE_KEY.remove(deps.storage);
    Ok(Response::new().add_attribute("migrated", "v1->v2"))
}

5. 代理模式(Proxy Pattern)

5.1 什么是代理模式

代理模式是一种合约架构,其中存在两个合约:

5.2 CW-* 代理合约模式

在 CosmWasm 中,代理模式通过消息转发实现:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ProxyState {
    pub owner: Addr,
    pub logic_contract: Addr,
    pub logic_code_id: u64,
    pub admin: Addr,
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Upgrade { new_logic, migrate_msg } => {
            let state = STATE.load(deps.storage)?;
            if info.sender != state.admin {
                return Err(ContractError::Unauthorized {});
            }
            STATE.update(deps.storage, |mut s| {
                s.logic_contract = deps.api.addr_validate(&new_logic)?;
                Ok(s)
            })?;
            Ok(Response::new()
                .add_message(WasmMsg::Execute {
                    contract_addr: new_logic,
                    msg: Binary::from(migrate_msg.as_slice()),
                    funds: vec![],
                })
                .add_attribute("action", "upgrade")
                .add_attribute("new_logic", new_logic))
        }
        _ => {
            let state = STATE.load(deps.storage)?;
            Ok(Response::new()
                .add_message(WasmMsg::Execute {
                    contract_addr: state.logic_contract.to_string(),
                    msg: to_binary(&msg)?,
                    funds: info.funds,
                }))
        }
    }
}

5.3 透明代理 vs UUPS

特性 透明代理(Transparent) UUPS
升级逻辑位置 代理合约 逻辑合约
Gas 开销 每次调用多一次消息转发 一次消息转发
风险 代理合约成为高价值目标 升级功能丢失则合约永久冻结
CosmWasm 实现 通过 WasmMsg::Execute 转发 逻辑合约自身处理 migrate

透明代理转发需传递原始调用者信息:

pub fn execute_proxy_forward(
    deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    let state = STATE.load(deps.storage)?;
    let wrapped_msg = ForwardedExecute {
        sender: info.sender.to_string(),
        funds: info.funds,
        msg: msg,
    };
    Ok(Response::new()
        .add_message(WasmMsg::Execute {
            contract_addr: state.logic_contract.to_string(),
            msg: to_binary(&wrapped_msg)?,
            funds: vec![],
        }))
}

5.4 与 Cosmos SDK 升级的区别

维度 CosmWasm 合约升级 Cosmos SDK 模块升级
影响范围 单个合约实例 整条链
操作方式 交易(Tx) 链升级(二进制替换)
治理需求 合约级治理或多签 链级治理
回滚方式 migrate 回旧 code_id 二进制回滚
原子性 交易级别 区块级别

5.5 代理模式 vs Migrate 模式的选择

考量 直接 Migrate 代理模式
部署复杂度 低(单合约) 高(双合约)
Gas 开销 正常 每次调用额外 Gas
升级灵活性 每次需迁移状态 可零状态升级
回滚速度 需迁移回旧版本 只需切换代理指向

在 MSG Chain 的 AI Agent 场景中,推荐使用直接 Migrate 模式作为默认选择。MSG Chain 的 genesis_registry_v1 已提供注册中心级别的版本管理,不需要代理层。


6. 自动迁移治理

6.1 DAO 提案驱动的升级流程

提交治理提案:

{
  "title": "升级 AI Agent 合约 v1.2.0",
  "description": "本次升级内容:\\n1. 优化任务调度算法\\n2. 新增优先级队列支持",
  "messages": [
    {
      "@type": "/cosmwasm.wasm.v1.MsgMigrateContract",
      "sender": "msg1governance_module...",
      "contract": "msg1contract_current...",
      "code_id": "6",
      "msg": "eyJ2ZXJzaW9uIjoiMi4wLjAiLCJtaWdyYXRlX3N0YXRlIjp0cnVlfQ=="
    }
  ],
  "deposit": "10000000umsg"
}
msg-chaind tx gov submit-proposal \
  --title "升级 AI Agent 合约 v1.2.0" \
  --description "..." \
  --type CosmWasmMigrateContract \
  --contract msg1contract_current... \
  --code-id 6 \
  --msg "$(base64 migrate_msg.json)" \
  --deposit 10000000umsg \
  --from proposer \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org \
  --gas auto --gas-prices 1000000000attoMSG -y

投票:

msg-chaind tx gov vote <proposal_id> yes \
  --from validator \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org -y

6.2 多签迁移保护

主网合约的 admin 必须设置为多签地址:

MAINNET_MULTISIG = {
    'threshold': '2_of_3',
    'signers': [
        'msg1primary_sign...',
        'msg1backup_sig...',
        'msg1emergency...',
    ],
}

多签迁移工作流:

class MultisigMigration:
    def __init__(self, chain_id: str, contract_addr: str, new_code_id: int):
        self.chain_id = chain_id
        self.contract = contract_addr
        self.new_code_id = new_code_id
        self.signatures = []

    def create_migration_tx(self) -> Dict:
        migrate_msg = {
            "@type": "/cosmwasm.wasm.v1.MsgMigrateContract",
            "sender": "msg1multisig_address...",
            "contract": self.contract,
            "code_id": str(self.new_code_id),
            "msg": base64.b64encode(
                json.dumps({"version": "2.0.0", "migrate_state": True}).encode()
            ).decode(),
        }
        return migrate_msg

    def collect_signature(self, signer: str, signature: str):
        self.signatures.append({
            'signer': signer, 'signature': signature,
            'timestamp': datetime.utcnow().isoformat(),
        })

    def submit_migration(self) -> Dict:
        if len(self.signatures) < 2:
            raise ValueError(f"Need 2 signatures, got {len(self.signatures)}")
        return {'status': 'submitted', 'tx_hash': '...'}

6.3 Governance Template

MSG Chain 的 execution_pack 提供了 governance_templates.json:

{
  "schema_version": "v1",
  "templates": [
    {
      "id": "contract_migrate_proposal",
      "title": "合约迁移治理提案模板",
      "messages": [
        {
          "@type": "/cosmwasm.wasm.v1.MsgMigrateContract",
          "description": "迁移 {contract_name} 到 code_id {new_code_id}"
        }
      ],
      "required_deposit": "10000000umsg",
      "voting_period": "7 days",
      "threshold": "quorum 0.2, threshold 0.5",
      "execution_type": "immediate"
    }
  ]
}

6.4 升级时间锁

对于高风险升级,建议引入时间锁机制:

pub fn execute_timelocked_migrate(
    deps: DepsMut, env: Env, proposal_id: u64,
) -> Result<Response, ContractError> {
    let upgrade = UPGRADE_TIMELOCK.load(deps.storage, proposal_id)?;

    if env.block.time < upgrade.execution_earliest {
        return Err(ContractError::TimelockNotExpired {
            remaining: (upgrade.execution_earliest - env.block.time).seconds(),
        });
    }
    if upgrade.executed {
        return Err(ContractError::UpgradeAlreadyExecuted {});
    }

    UPGRADE_TIMELOCK.update(deps.storage, proposal_id, |mut u| {
        u.executed = true; Ok(u)
    })?;

    Ok(Response::new()
        .add_message(WasmMsg::Migrate {
            contract_addr: upgrade.target_contract.to_string(),
            new_code_id: upgrade.new_code_id,
            msg: upgrade.migrate_msg,
        }))
}

6.5 升级权限模型

环境 推荐权限模型 理由
Local SingleAdmin 开发迭代速度快
Testnet SingleAdmin 或 Multisig(1/2) 调试方便
Mainnet Multisig(2/3) 或 DAO 资金安全,去中心化治理

7. 回滚与应急回退

7.1 回滚的必要性

常见故障场景:

7.2 回滚机制

回滚本质上是一次特殊的迁移:

msg-chaind tx wasm migrate $CONTRACT_ADDR $PREVIOUS_CODE_ID \
  '{"version":"rollback","migrate_state":true}' \
  --from multisig \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org \
  --gas auto --gas-prices 1000000000attoMSG -y

回滚合约的关键问题:

  1. 旧版本的 migrate 入口点需要能处理回滚消息
  2. 如果新版本修改了存储结构,回滚时需要转回旧格式
  3. 回滚后 CW2 版本应当反映回滚后的版本
pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let ver = cw2::get_contract_version(deps.storage)?;

    match msg.version.as_str() {
        "2.0.0" => {
            ensure_eq!(ver.version, "1.0.0", ContractError::WrongVersion);
            migrate_v1_to_v2(deps, env, &msg)
        }
        "rollback" | "1.0.0" => {
            verify_rollback_authority(deps.as_ref(), &env)?;
            rollback_v2_to_v1(deps, env, &msg)
        }
        other => Err(ContractError::UnsupportedVersion(other.to_string())),
    }
}

fn rollback_v2_to_v1(deps: DepsMut, env: Env, msg: &MigrateMsg) -> Result<Response, ContractError> {
    let v2_state: StateV2 = STATE_V2.load(deps.storage)?;
    let v1_state = OldState {
        owner: v2_state.owner,
        total_tasks: v2_state.total_tasks,
        completed_tasks: v2_state.completed_tasks,
        created_at: v2_state.created_at,
    };
    OLD_STATE_KEY.save(deps.storage, &v1_state)?;
    STATE_V2.remove(deps.storage);
    cw2::set_contract_version(deps.storage, CONTRACT_NAME, "1.0.0")?;
    Ok(Response::new()
        .add_attribute("action", "rollback")
        .add_attribute("from", "2.0.0")
        .add_attribute("to", "1.0.0"))
}

7.3 reply on / atomic rollback

MSG Chain 的 CosmWasm 运行时支持 reply_on 机制:

pub fn execute_with_reply(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, ContractError> {
    let migrate_msg = WasmMsg::Migrate {
        contract_addr: "msg1target_contract...".to_string(),
        new_code_id: 5,
        msg: to_binary(&MigrateMsg { version: "2.0.0".to_string(), migrate_state: true })?,
    };

    let sub_msg = SubMsg::reply_on_success(migrate_msg, REPLY_MIGRATE_ID);
    Ok(Response::new().add_submessage(sub_msg))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
    match msg.id {
        REPLY_MIGRATE_ID => {
            if msg.result.is_ok() {
                let data = msg.result.unwrap().data.unwrap_or_default();
                let response: MigrateResponse = from_binary(&data)?;
                validate_migration(&response)?;
                Ok(Response::new().add_attribute("migration", "confirmed"))
            } else {
                Err(ContractError::MigrationFailed("migration submsg failed".to_string()))
            }
        }
        _ => Err(ContractError::UnknownReplyId(msg.id)),
    }
}

原子回滚保证:

7.4 注册中心降级策略

当合约迁移后出现问题,除了合约层面回滚,还需更新注册中心映射:

class RegistryDowngrade:
    def __init__(self, registry_addr: str, rpc: str):
        self.registry = registry_addr
        self.rpc = rpc

    def downgrade(self, canonical_key: str, previous_code_id: int) -> Dict:
        return {
            'action': 'registry_downgrade',
            'canonical_key': canonical_key,
            'new_code_id': previous_code_id,
            'note': '注册中心降级不会自动恢复合约状态,需要额外的 migrate 操作',
        }

    def emergency_pause(self, canonical_key: str) -> Dict:
        return {
            'action': 'emergency_pause',
            'canonical_key': canonical_key,
            'effect': '此密钥的地址解析将返回暂停状态',
        }

7.5 回滚计划模板

{
  "schema_version": "v1",
  "contract": "msg1ai_agent...",
  "upgrade": {
    "from_code_id": 4, "to_code_id": 5,
    "from_version": "1.0.0", "to_version": "2.0.0"
  },
  "rollback_plan": {
    "triggers": ["critical_bug", "state_corruption", "economic_attack"],
    "steps": [
      {"order": 1, "action": "freeze_contract", "requires": "multisig"},
      {"order": 2, "action": "snapshot_state", "requires": "read_only"},
      {"order": 3, "action": "rollback_migrate", "requires": "multisig"},
      {"order": 4, "action": "verify_state", "requires": "read_only"},
      {"order": 5, "action": "notify_stakeholders", "requires": "manual"},
      {"order": 6, "action": "post_mortem", "requires": "manual"}
    ],
    "estimated_time_minutes": 30,
    "requires_multisig": true
  }
}

8. 升级测试

8.1 为什么需要在测试环境中测试迁移

合约迁移涉及状态转换,测试覆盖度直接影响主网安全性。测试目标:

8.2 在 cw-multi-test 中编写迁移测试

测试基础设施:

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::*;
    use cw_multi_test::{App, Contract, ContractWrapper, Executor};

    fn contract_v1() -> Box<dyn Contract<Empty>> {
        let contract = ContractWrapper::new(
            crate::v1::execute, crate::v1::instantiate, crate::v1::query,
        ).with_migrate(crate::v1::migrate);
        Box::new(contract)
    }

    fn contract_v2() -> Box<dyn Contract<Empty>> {
        let contract = ContractWrapper::new(
            crate::v2::execute, crate::v2::instantiate, crate::v2::query,
        ).with_migrate(crate::v2::migrate);
        Box::new(contract)
    }

    fn setup_test() -> (App, Addr) {
        let mut app = App::default();
        let v1_code_id = app.store_code(contract_v1());
        let contract_addr = app.instantiate_contract(
            v1_code_id, Addr::unchecked("owner"),
            &v1::InstantiateMsg { owner: "owner".to_string(), config: v1::Config { fee_bps: 50, cooldown: 3600 }},
            &[], "AI Agent v1", Some("owner".to_string()),
        ).unwrap();
        (app, contract_addr)
    }
}

测试迁移流程:

#[test]
fn test_basic_migration() {
    let (mut app, contract_addr) = setup_test();

    let v1_config: v1::ConfigResponse = app
        .wrap().query_wasm_smart(contract_addr.clone(), &v1::QueryMsg::Config {}).unwrap();
    assert_eq!(v1_config.fee_bps, 50);

    let v2_code_id = app.store_code(contract_v2());

    app.migrate_contract(
        Addr::unchecked("owner"), contract_addr.clone(), v2_code_id,
        &v2::MigrateMsg {
            version: "2.0.0".to_string(), migrate_state: true,
            config: Some(v2::Config { fee_bps: 30, cooldown: 7200, max_gas_per_task: 500000 }),
            new_admin: None, hook_params: None,
        },
    ).unwrap();

    let v2_config: v2::ConfigResponse = app
        .wrap().query_wasm_smart(contract_addr.clone(), &v2::QueryMsg::Config {}).unwrap();
    assert_eq!(v2_config.fee_bps, 30);
    assert_eq!(v2_config.cooldown, 7200);
    assert_eq!(v2_config.max_gas_per_task, 500000);
}

测试权限控制:

#[test]
fn test_migration_unauthorized() {
    let (mut app, contract_addr) = setup_test();
    let v2_code_id = app.store_code(contract_v2());

    let err = app.migrate_contract(
        Addr::unchecked("unauthorized_user"), contract_addr.clone(), v2_code_id,
        &v2::MigrateMsg { version: "2.0.0".to_string(), migrate_state: true,
            config: None, new_admin: None, hook_params: None },
    ).unwrap_err();

    assert!(err.to_string().contains("Unauthorized"));
}

测试数据迁移完整性:

#[test]
fn test_data_migration_integrity() {
    let (mut app, contract_addr) = setup_test();

    for i in 0..100 {
        app.execute_contract(
            Addr::unchecked("user1"), contract_addr.clone(),
            &v1::ExecuteMsg::AddTask { description: format!("task {}", i) }, &[],
        ).unwrap();
    }

    let v2_code_id = app.store_code(contract_v2());
    app.migrate_contract(
        Addr::unchecked("owner"), contract_addr.clone(), v2_code_id,
        &v2::MigrateMsg { version: "2.0.0".to_string(), migrate_state: true,
            config: None, new_admin: None, hook_params: None },
    ).unwrap();

    let v2_tasks: Vec<v2::TaskResponse> = app
        .wrap().query_wasm_smart(contract_addr.clone(),
            &v2::QueryMsg::ListTasks { start_after: None, limit: Some(200) }).unwrap();
    assert_eq!(v2_tasks.len(), 100);
}

测试回滚:

#[test]
fn test_rollback() {
    let (mut app, contract_addr) = setup_test();
    let v2_code_id = app.store_code(contract_v2());

    app.migrate_contract(
        Addr::unchecked("owner"), contract_addr.clone(), v2_code_id,
        &v2::MigrateMsg { version: "2.0.0".to_string(), migrate_state: true,
            config: None, new_admin: None, hook_params: None },
    ).unwrap();

    let v1_code_id = 1;
    app.migrate_contract(
        Addr::unchecked("owner"), contract_addr.clone(), v1_code_id,
        &v1::MigrateMsg { version: "rollback".to_string(), migrate_state: true },
    ).unwrap();

    let config_v1_after: v1::ConfigResponse = app
        .wrap().query_wasm_smart(contract_addr.clone(), &v1::QueryMsg::Config {}).unwrap();
    assert_eq!(config_v1_after.fee_bps, 50);
}

8.3 测试数据兼容性

#[test]
fn test_query_compatibility() {
    let (mut app, contract_addr) = setup_test();
    let v2_code_id = app.store_code(contract_v2());

    app.migrate_contract(
        Addr::unchecked("owner"), contract_addr.clone(), v2_code_id,
        &v2::MigrateMsg { version: "2.0.0".to_string(), migrate_state: true,
            config: None, new_admin: None, hook_params: None },
    ).unwrap();

    let v1_result: v1::TaskCountResponse = app
        .wrap().query_wasm_smart(contract_addr.clone(), &v1::QueryMsg::TaskCount {}).unwrap();
}

9. AI Agent 升级工作流

9.1 自动化升级流水线

┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│  代码变更    │ → │  编译构建    │ → │  cw-multi-  │ → │  Testnet    │
│  (Git PR)   │    │  (CI/CD)    │    │  test 测试   │    │  部署验证    │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘
                                                              │
┌─────────────┐    ┌─────────────┐    ┌─────────────┐         │
│  监控验证    │ ← │  主网迁移    │ ← │  多签审批    │ ←────────┘
│  (24h)      │    │  (治理)     │    │  (人工)     │
└─────────────┘    └─────────────┘    └─────────────┘

流水线各阶段:

# .github/workflows/upgrade-pipeline.yaml
name: AI Agent Upgrade Pipeline
on:
  pull_request:
    branches: [main]
    paths: ['contracts/ai-agent/**']

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Rust
        uses: actions-rust-lang/setup-rust-toolchain@v1
        with: { target: wasm32-unknown-unknown }
      - name: Build & Test
        run: |
          cd contracts/ai-agent
          cargo build --release --target wasm32-unknown-unknown --locked
          cargo test
      - name: CW2 Version Check
        run: |
          grep -q 'CONTRACT_VERSION.*"[0-9]\+.[0-9]\+.[0-9]\+"' contracts/ai-agent/src/contract.rs
      - name: Generate Migration Test Report
        run: |
          cargo test --test migration_tests -- --nocapture > migration_report.txt
          cat migration_report.txt

升级制品管理:

class UpgradeArtifact:
    def __init__(self, contract_name: str, version: str):
        self.contract_name = contract_name
        self.version = version
        self.timestamp = datetime.utcnow().isoformat()
        self.sha256 = None
        self.code_id = None
        self.evidence = []

    def build(self, source_dir: str) -> str:
        import subprocess, hashlib
        result = subprocess.run(
            ["cargo", "build", "--release", "--target", "wasm32-unknown-unknown", "--locked"],
            cwd=source_dir, capture_output=True, text=True
        )
        if result.returncode != 0:
            raise RuntimeError(f"Build failed: {result.stderr}")
        wasm_path = f"{source_dir}/target/wasm32-unknown-unknown/release/{self.contract_name}.wasm"
        with open(wasm_path, "rb") as f:
            wasm_bytes = f.read()
        self.sha256 = hashlib.sha256(wasm_bytes).hexdigest()
        self.evidence.append({
            "type": "build_artifact", "sha256": self.sha256, "timestamp": self.timestamp,
        })
        return wasm_path

    def generate_manifest(self) -> Dict:
        return {
            "schema_version": "v1",
            "contract": self.contract_name, "version": self.version,
            "sha256": self.sha256, "timestamp": self.timestamp,
            "migration_type": "direct_migrate",
            "cw2_compatible": True, "state_migration_required": True,
            "evidence": self.evidence,
        }

9.2 升级协调器

class UpgradeCoordinator:
    def __init__(self, contract_addr: str, chain_id: str, rpc: str):
        self.contract = contract_addr
        self.chain_id = chain_id
        self.rpc = rpc
        self.state = "idle"
        self.history = []

    async def execute_upgrade(self, new_code_id: int, migrate_msg: Dict, approval_gate: Dict) -> Dict:
        self._transition("precheck")
        precheck = await self._pre_migrate_check(new_code_id)
        if not precheck["passed"]:
            return {"status": "blocked", "reason": precheck["failures"]}

        self._transition("snapshot")
        snapshot = await self._snapshot_state()
        self.history.append({"phase": "snapshot", "data": snapshot})

        self._transition("approval")
        if approval_gate.get("requires_human"):
            return {
                "status": "awaiting_approval", "phase": "secret_injection",
                "requires": ["签名账户", "Gas 预算确认", "多签签名"],
                "contract": self.contract, "new_code_id": new_code_id,
                "migrate_msg": migrate_msg,
            }

        self._transition("migrate")
        result = await self._execute_migrate(new_code_id, migrate_msg)
        self.history.append({"phase": "migrate", "tx_hash": result["tx_hash"]})

        self._transition("verify")
        verification = await self._post_migrate_verify()
        if not verification["passed"]:
            return await self._auto_rollback(precheck["current_code_id"])

        self._transition("registry_update")
        await self._update_registry(new_code_id)

        self._transition("completed")
        return {"status": "completed", "tx_hash": result["tx_hash"],
                "new_code_id": new_code_id, "verification": verification}

    def _transition(self, new_state: str):
        self.state = new_state
        self.history.append({"state": new_state, "timestamp": datetime.utcnow().isoformat()})

    async def _pre_migrate_check(self, new_code_id: int) -> Dict:
        checks = {}
        contract_info = await self._query_contract_info()
        checks["contract_exists"] = contract_info is not None
        code_info = await self._query_code_info(new_code_id)
        checks["code_exists"] = code_info is not None
        checks["admin_set"] = contract_info.get("admin") is not None
        current_code_id = contract_info.get("code_id")
        checks["different_code_id"] = current_code_id != new_code_id
        return {"passed": all(checks.values()),
                "failures": [k for k, v in checks.items() if not v],
                "current_code_id": current_code_id}

9.3 金丝雀部署

class CanaryRelease:
    def __init__(self, contract_name: str):
        self.contract_name = contract_name
        self.phases = [
            {
                "name": "internal_test", "env": "local",
                "verifiers": ["dev_team"], "duration_hours": 2,
                "success_criteria": {"all_tests_pass": True},
            },
            {
                "name": "testnet_canary", "env": "testnet",
                "verifiers": ["dev_team", "security_team"], "duration_hours": 24,
                "success_criteria": {"tx_success_rate": 0.999, "no_critical_errors": True},
            },
            {
                "name": "mainnet_canary", "env": "mainnet",
                "verifiers": ["multisig_signers", "dao"], "duration_hours": 72,
                "success_criteria": {"tx_success_rate": 0.999, "gas_increase_pct": 10},
            },
            {
                "name": "full_rollout", "env": "mainnet",
                "verifiers": ["dao"], "duration_hours": None,
            },
        ]
        self.current_phase = 0

    async def evaluate_phase(self, monitoring_data: Dict) -> Dict:
        phase = self.phases[self.current_phase]
        criteria = phase["success_criteria"]
        evaluations = {}
        for criterion, threshold in criteria.items():
            actual = monitoring_data.get(criterion)
            if criterion == "tx_success_rate":
                evaluations[criterion] = actual >= threshold
            elif criterion == "gas_increase_pct":
                evaluations[criterion] = actual <= threshold
            else:
                evaluations[criterion] = actual == threshold

        all_pass = all(evaluations.values())
        if all_pass and self.current_phase < len(self.phases) - 1:
            self.current_phase += 1
            return {"status": "advancing", "next_phase": self.phases[self.current_phase]["name"]}
        elif all_pass:
            return {"status": "completed"}
        else:
            return {"status": "blocked", "phase": phase["name"],
                    "failed": [k for k, v in evaluations.items() if not v]}

9.4 功能开关

合约内内置功能开关,通过消息启用或禁用:

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FeatureFlags {
    pub use_new_scoring_algorithm: bool,
    pub enable_priority_queue: bool,
    pub use_v2_task_format: bool,
}

pub const FEATURE_FLAGS: Item<FeatureFlags> = Item::new("feature_flags");

pub fn execute_set_feature(
    deps: DepsMut, info: MessageInfo, feature: String, enabled: bool,
) -> Result<Response, ContractError> {
    let admin = ADMIN.load(deps.storage)?;
    if info.sender != admin {
        return Err(ContractError::Unauthorized {});
    }
    FEATURE_FLAGS.update(deps.storage, |mut flags| {
        match feature.as_str() {
            "new_scoring" => flags.use_new_scoring_algorithm = enabled,
            "priority_queue" => flags.enable_priority_queue = enabled,
            "v2_format" => flags.use_v2_task_format = enabled,
            _ => return Err(ContractError::UnknownFeature(feature)),
        }
        Ok(flags)
    })?;
    Ok(Response::new().add_attribute("feature", feature).add_attribute("enabled", enabled.to_string()))
}

9.5 升级后监控

class PostUpgradeMonitor:
    def __init__(self, contract_addr: str, rpc: str):
        self.contract = contract_addr
        self.rpc = rpc

    async def monitor(self, duration_blocks: int = 1000) -> Dict:
        start_height = await self._current_height()
        end_height = start_height + duration_blocks
        observations = []
        while (await self._current_height()) < end_height:
            block_obs = await self._observe_block()
            observations.append(block_obs)
            await asyncio.sleep(6)
        return self._analyze(observations)

    def _analyze(self, observations: List[Dict]) -> Dict:
        total = len(observations)
        failed = sum(1 for o in observations if not o.get("config_readable", True))
        return {
            "status": "healthy" if failed == 0 else "degraded",
            "total_blocks": total, "failed_queries": failed,
            "health_score": (total - failed) / total * 100,
            "recommendation": "stable" if failed == 0 else "consider_rollback",
        }

10. 实战:AI Agent 合约版本演进案例

10.1 v1:初始版本

AI Agent 合约的第一个版本,功能包括:

// v1 存储结构
pub const CONFIG: Item<ConfigV1> = Item::new("config_v1");
pub const TASKS: Map<u64, TaskV1> = Map::new("tasks_v1");
pub const NEXT_TASK_ID: Item<u64> = Item::new("next_task_id_v1");

pub struct ConfigV1 {
    pub owner: Addr,
    pub fee_bps: u16,
    pub cooldown: u64,
}

pub struct TaskV1 {
    pub id: u64,
    pub description: String,
    pub completed: bool,
    pub assignee: Addr,
    pub created_at: Timestamp,
    pub completed_at: Option<Timestamp>,
}

pub const CONTRACT_NAME: &str = "crates.io:ai-agent";
pub const CONTRACT_VERSION: &str = "1.0.0";

10.2 v2:引入优先级和标签

v2 新增:任务优先级、标签系统、任务统计。

v2 存储结构变更:

pub struct ConfigV2 {
    pub owner: Addr,
    pub fee_bps: u16,
    pub cooldown: u64,
    pub max_priority: u8,
    pub allowed_tags: Vec<String>,
    pub max_gas_per_task: u64,
}

pub struct TaskV2 {
    pub id: u64,
    pub description: String,
    pub status: TaskStatus,
    pub assignee: Addr,
    pub created_at: Timestamp,
    pub completed_at: Option<Timestamp>,
    pub tags: Vec<String>,
    pub priority: u8,
}

pub enum TaskStatus { Pending, InProgress, Completed, Cancelled }

pub struct Statistics {
    pub total_tasks: u64,
    pub completed_tasks: u64,
    pub cancelled_tasks: u64,
    pub in_progress_tasks: u64,
    pub total_tags_used: u64,
}

v2 迁移逻辑:

pub fn migrate_v1_to_v2(deps: DepsMut, env: Env, msg: &MigrateMsg) -> Result<Response, ContractError> {
    let v1_config = CONFIG_V1.load(deps.storage)?;

    let v2_config = ConfigV2 {
        owner: v1_config.owner,
        fee_bps: msg.config.as_ref().map(|c| c.fee_bps).unwrap_or(v1_config.fee_bps),
        cooldown: msg.config.as_ref().map(|c| c.cooldown).unwrap_or(v1_config.cooldown),
        max_priority: msg.config.as_ref().map(|c| c.max_priority).unwrap_or(10),
        allowed_tags: msg.config.as_ref().map(|c| c.allowed_tags.clone()).unwrap_or_default(),
        max_gas_per_task: msg.config.as_ref().map(|c| c.max_gas_per_task).unwrap_or(300000),
    };
    CONFIG_V2.save(deps.storage, &v2_config)?;

    let mut stats = Statistics {
        total_tasks: 0, completed_tasks: 0, cancelled_tasks: 0,
        in_progress_tasks: 0, total_tags_used: 0,
    };

    let v1_tasks: Vec<(u64, TaskV1)> = TASKS_V1
        .range(deps.storage, None, None, Order::Ascending)
        .collect::<StdResult<Vec<_>>>()?;

    for (id, v1_task) in &v1_tasks {
        let v2_task = TaskV2 {
            id: *id,
            description: v1_task.description.clone(),
            status: if v1_task.completed { TaskStatus::Completed } else { TaskStatus::Pending },
            assignee: v1_task.assignee.clone(),
            created_at: v1_task.created_at,
            completed_at: v1_task.completed_at,
            tags: vec![], priority: 0,
        };
        TASKS_V2.save(deps.storage, *id, &v2_task)?;
        stats.total_tasks += 1;
        if v1_task.completed { stats.completed_tasks += 1; }
    }

    STATISTICS.save(deps.storage, &stats)?;
    let next_id = NEXT_TASK_ID_V1.load(deps.storage)?;
    NEXT_TASK_ID_V2.save(deps.storage, &next_id)?;

    CONFIG_V1.remove(deps.storage);
    for (id, _) in &v1_tasks { TASKS_V1.remove(deps.storage, *id); }
    NEXT_TASK_ID_V1.remove(deps.storage);

    set_contract_version(deps.storage, CONTRACT_NAME, "2.0.0")?;

    Ok(Response::new()
        .add_attribute("action", "migrate")
        .add_attribute("from", "1.0.0").add_attribute("to", "2.0.0")
        .add_attribute("tasks_migrated", stats.total_tasks.to_string()))
}

10.3 v3:引入 AI 评分引擎

v3 重大变更:AI 评分引擎、信誉系统、版本化存储。

pub fn migrate_v2_to_v3(deps: DepsMut, env: Env, msg: &MigrateMsg) -> Result<Response, ContractError> {
    let v2_config = CONFIG_V2.load(deps.storage)?;
    let v2_tasks: Vec<(u64, TaskV2)> = TASKS_V2
        .range(deps.storage, None, None, Order::Ascending)
        .collect::<StdResult<Vec<_>>>()?;

    let v3_config = ConfigV3 {
        owner: v2_config.owner,
        fee_bps: v2_config.fee_bps,
        cooldown: v2_config.cooldown,
        max_priority: v2_config.max_priority,
        allowed_tags: v2_config.allowed_tags,
        max_gas_per_task: v2_config.max_gas_per_task,
        scoring_enabled: true,
        scoring_model_hash: "".to_string(),
        reputation_decay_blocks: 10000,
    };
    CONFIG_V3.save(deps.storage, &v3_config)?;

    for (id, v2_task) in &v2_tasks {
        let v3_task = VersionedTask::V2(v2_task.clone());
        TASKS_V3.save(deps.storage, *id, &v3_task)?;
    }

    CONFIG_V2.remove(deps.storage);
    for (id, _) in &v2_tasks { TASKS_V2.remove(deps.storage, *id); }

    set_contract_version(deps.storage, CONTRACT_NAME, "3.0.0")?;
    Ok(Response::new()
        .add_attribute("action", "migrate")
        .add_attribute("from", "2.0.0").add_attribute("to", "3.0.0")
        .add_attribute("tasks_migrated", v2_tasks.len().to_string()))
}

10.4 跨版本兼容的查询接口

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    #[serde(rename = "get_task")]
    GetTaskV1 { task_id: u64 },
    GetTaskV2 { task_id: u64 },
    GetTaskV3 { task_id: u64 },
    GetReputation { address: String },
}

pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetTaskV1 { task_id } => {
            let task = query_task_internal(deps, task_id)?;
            to_binary(&TaskV1Response {
                id: task.id, description: task.description,
                completed: matches!(task.status, TaskStatus::Completed),
                assignee: task.assignee.to_string(),
            })
        }
        QueryMsg::GetTaskV2 { task_id } => {
            let task = query_task_internal(deps, task_id)?;
            to_binary(&TaskV2Response {
                id: task.id, description: task.description,
                status: format!("{:?}", task.status),
                assignee: task.assignee.to_string(),
                tags: task.tags, priority: task.priority,
            })
        }
        QueryMsg::GetTaskV3 { task_id } => {
            let task = query_task_internal(deps, task_id)?;
            to_binary(&task)
        }
        _ => Err(StdError::generic_err("unknown query")),
    }
}

10.5 迁移验证清单

MIGRATION_VERIFICATION_CHECKLIST = [
    {"id": "M001", "item": "新版本合约编译通过", "automated": True},
    {"id": "M002", "item": "CW2 版本号已递增", "automated": True},
    {"id": "M003", "item": "CONTRACT_NAME 不变", "automated": True},
    {"id": "M004", "item": "cw-multi-test 迁移测试通过", "automated": True},
    {"id": "M005", "item": "数据完整性测试通过(100 条以上)", "automated": True},
    {"id": "M006", "item": "回滚测试通过", "automated": True},
    {"id": "M007", "item": "权限控制测试通过", "automated": True},
    {"id": "M008", "item": "查询向前兼容性测试通过", "automated": True},
    {"id": "M009", "item": "Testnet 迁移验证完成", "automated": False},
    {"id": "M010", "item": "Testnet 稳定运行 >=72 小时", "automated": True},
    {"id": "M011", "item": "Gas 消耗在预期范围内", "automated": True},
    {"id": "M012", "item": "合约 admin 设置为多签地址", "automated": False},
    {"id": "M013", "item": "迁移消息已在多签中预演签署", "automated": False},
    {"id": "M014", "item": "回滚计划和回滚消息已准备", "automated": False},
    {"id": "M015", "item": "治理提案已提交并投票通过", "automated": False},
    {"id": "M016", "item": "升级时间锁已到期", "automated": True},
    {"id": "M017", "item": "迁移后监控脚本已就绪", "automated": True},
    {"id": "M018", "item": "升级完成通知已配置", "automated": True},
]

11. 安全注意事项

11.1 迁移钩子的权限控制

迁移入口点必须进行严格的权限校验:

pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    // 方法 1:检查合约 admin
    let contract_info = deps.querier.query_wasm_contract_info(env.contract.address.clone())?;
    let admin = contract_info.admin.ok_or(ContractError::NoAdmin {})?;
    if info.sender != admin {
        return Err(ContractError::Unauthorized {});
    }

    // 方法 2:检查合约内的 owner 字段
    let config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner {
        return Err(ContractError::Unauthorized {});
    }

    // 方法 3:多重检查(最安全)
    if info.sender != config.owner && info.sender != admin {
        return Err(ContractError::Unauthorized {});
    }
}

安全建议:

11.2 存储 key 冲突

新旧版本如果使用相同的存储 key 前缀,可能导致数据损坏:

// 危险:新旧版本使用相同 key
pub const STATE: Item<StateV1> = Item::new("state");
// v2 也使用相同的 key
pub const STATE: Item<StateV2> = Item::new("state");  // 冲突!

// 安全:使用不同的前缀
pub const STATE_V1: Item<StateV1> = Item::new("state_v1");
pub const STATE_V2: Item<StateV2> = Item::new("state_v2");

迁移后的 key 清理:

fn cleanup_old_storage(deps: DepsMut) -> Result<(), ContractError> {
    CONFIG_V1.remove(deps.storage);
    STATE_V1.remove(deps.storage);
    let old_keys: Vec<u64> = TASKS_V1
        .keys(deps.storage, None, None, Order::Ascending)
        .collect::<StdResult<Vec<_>>>()?;
    for key in old_keys { TASKS_V1.remove(deps.storage, key); }
    Ok(())
}

11.3 初始化保护

pub fn instantiate(deps: DepsMut, env: Env, info: MessageInfo, msg: InstantiateMsg) -> Result<Response, ContractError> {
    if CONTRACT_VERSION.may_load(deps.storage)?.is_some() {
        return Err(ContractError::AlreadyInitialized {});
    }
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
}

pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let existing = CONTRACT_VERSION.may_load(deps.storage)?;
    if existing.is_none() {
        return Err(ContractError::NotInitialized {});
    }
}

11.4 CW2 版本验证

pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let ver = cw2::get_contract_version(deps.storage)?;

    if ver.contract != CONTRACT_NAME {
        return Err(ContractError::ContractMismatch {
            expected: CONTRACT_NAME.to_string(), actual: ver.contract,
        });
    }

    let from = semver::Version::parse(&ver.version)
        .map_err(|_| ContractError::InvalidVersion(ver.version.clone()))?;
    let to = semver::Version::parse(CONTRACT_VERSION)
        .map_err(|_| ContractError::InvalidVersion(CONTRACT_VERSION.to_string()))?;

    if to <= from {
        return Err(ContractError::VersionNotIncreased {
            from: ver.version, to: CONTRACT_VERSION.to_string(),
        });
    }

    let major_from = from.major;
    let major_to = to.major;
    if major_to > major_from + 1 {
        return Err(ContractError::VersionJumpTooLarge {
            from: ver.version, to: CONTRACT_VERSION.to_string(),
        });
    }
}

11.5 跨合约调用中的升级安全

pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let notify = WasmMsg::Execute {
        contract_addr: REGISTRY_ADDR.to_string(),
        msg: to_binary(&RegistryExecuteMsg::NotifyUpgrade {
            contract: env.contract.address.to_string(),
            new_version: CONTRACT_VERSION.to_string(),
        })?,
        funds: vec![],
    };
    Ok(Response::new()
        .add_message(notify)
        .add_attribute("action", "migrate"))
}

11.6 资金安全

pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let balance = deps.querier.query_balance(&env.contract.address, "umsg")?;

    MIGRATION_LOG.save(deps.storage, &MigrationLogEntry {
        pre_migrate_balance: balance.amount,
        timestamp: env.block.time,
    })?;

    let post_balance = deps.querier.query_balance(&env.contract.address, "umsg")?;
    if post_balance.amount != balance.amount {
        return Err(ContractError::BalanceChanged {
            before: balance.amount, after: post_balance.amount,
        });
    }
}

11.7 拒绝服务保护

pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let task_count = TASKS_V1.keys(deps.storage, None, None, Order::Ascending).count();

    if task_count > 1000 && !msg.force_migrate {
        return Err(ContractError::DataTooLarge {
            count: task_count as u64, max: 1000,
            suggestion: "使用懒迁移模式分批迁移".to_string(),
        });
    }
}

11.8 回滚安全

pub fn rollback(deps: DepsMut, env: Env, msg: &MigrateMsg) -> Result<Response, ContractError> {
    let admin = ADMIN.load(deps.storage)?;
    if info.sender != admin {
        return Err(ContractError::Unauthorized {});
    }

    ROLLBACK_LOG.save(deps.storage, &RollbackEntry {
        rolled_back_by: info.sender.to_string(),
        rolled_back_at: env.block.time,
        reason: msg.version.clone(),
        snapshot_hash: None,
    })?;

    let cooldown = env.block.time.plus_seconds(ROLLBACK_COOLDOWN_SECONDS);
    NEXT_UPGRADE_AT.save(deps.storage, &cooldown)?;
}

12. 总结

12.1 升级策略选择树

合约是否需要升级?
├── 否 → 保持不可变合约(最高安全级别)
│       场景:基础库合约、核心系统合约
│
└── 是 → 升级频率如何?
    ├── 低(数月一次)→ 使用直接 Migrate
    │       场景:AI Agent 的版本迭代
    │       优点:简单、Gas 高效、CW2 原生支持
    │
    └── 高(周级)→ 使用代理模式
            场景:实验性 AI 模型频繁迭代
            优点:无需迁移状态,切换逻辑合约即可
            缺点:额外 Gas、调用者身份传递复杂
数据迁移策略选择:
迁移数据量?
├── 少(< 100 条)→ 直接迁移(Inline Migration)
│
├── 中等(100-1000 条)→ 权衡:
│   ├── 如果 Gas 允许 → 直接迁移
│   └── 如果 Gas 紧张 → 懒迁移
│
└── 大(> 1000 条)→ 懒迁移(Lazy Migration)
        或使用版本化存储(新旧格式共存)
权限模型选择(按环境):
├── Local → SingleAdmin(开发效率优先)
├── Testnet → SingleAdmin 或 1/2 多签
└── Mainnet →
    ├── 核心资产合约 → DAO 治理提案 + 多签执行
    ├── AI Agent 逻辑合约 → 2/3 多签
    └── 无资产合约 → Multisig 或 Timelock
回滚策略选择:
风险等级?
├── 低风险(配置参数调整)→ 直接 migrate 回滚
├── 中风险(功能新增)→ migrate 回滚 + 注册中心降级
└── 高风险(核心逻辑变更)→
    多重防护:
    1. 时间锁(延迟执行)
    2. DAO 投票 + 多签
    3. 金丝雀部署
    4. 自动回滚预案

12.2 何时使用 migrate vs 新合约部署

场景 建议 理由
Bug 修复 Migrate 保持同一地址,用户无需迁移
功能增强 Migrate 保留状态和历史记录
参数调整 Migrate 低成本更新
存储结构变更 Migrate 需要数据迁移逻辑
完全重写 新合约部署 逻辑差异太大
安全重构 新合约部署 旧合约可能存在未发现的安全问题
协议不兼容的升级 新合约部署 无法保持向后兼容

12.3 MSG Chain 升级最佳实践

  1. 始终在 Testnet 验证迁移:稳定运行至少 72 小时
  2. 始终准备回滚计划:每个升级必须有对应的回滚方案
  3. 使用 CW2 标准版本追踪:永远不要绕过 cw2::set_contract_version
  4. 存储 key 版本化:使用带版本号的前缀,避免 key 冲突
  5. 保持查询向前兼容:至少在当前版本和上一个版本之间保持兼容
  6. 权限分离:迁移权限、管理权限、操作权限应分离
  7. Gas 预算管理:迁移逻辑应在 5M gas 内完成
  8. 原子回滚保障:利用 CosmWasm 的原子回滚特性
  9. 注册中心同步:迁移后更新 genesis_registry_v1 中的 code_id 映射
  10. 监控持续:迁移后保持至少 24 小时的监控观察期

12.4 升级就绪检查协议

UPGRADE_READINESS_PROTOCOL = {
    'schema_version': 'v1',
    'checks': [
        {'id': 'code_compiled', 'description': '新版本合约编译通过', 'automated': True},
        {'id': 'tests_passed', 'description': '迁移测试和数据完整性测试通过', 'automated': True},
        {'id': 'testnet_validated', 'description': 'Testnet 迁移验证完成', 'automated': False},
        {'id': 'rollback_plan_approved', 'description': '回滚计划已创建并批准', 'automated': False},
        {'id': 'approval_gates_passed', 'description': '审批门禁已通过', 'automated': False},
        {'id': 'multisig_ready', 'description': '多签签名已收集到阈值', 'automated': False},
        {'id': 'gas_estimated', 'description': '迁移交易 Gas 已估算', 'automated': True},
        {'id': 'evidence_collected', 'description': '迁移前状态快照已记录', 'automated': True},
    ],
    'result': None,
}

12.5 展望

随着 MSG Chain 生态的发展,合约升级模式也在演进。未来的方向包括:

AI Agent 作为 MSG Chain 上的自治实体,其合约升级不仅是一个技术操作,更是治理哲学和信任模型的体现。合理的升级策略使 AI Agent 能够在保持去中心化信任的同时,持续演进其能力。


附录

A. 参考 URL

资源 URL
MSG Chain RPC https://rpc.msgchain.org
MSG Chain REST https://api.msgchain.org
MSG Chain Testnet RPC https://rpc-testnet.msgchain.org
MSG Chain Explorer https://explorer.msgchain.org
MSG Chain 白皮书 https://msgchain.org/whitepaper
CosmWasm 文档 https://docs.cosmwasm.com
CW2 规范 https://github.com/CosmWasm/cw-plus/tree/main/packages/cw2
cw-multi-test https://docs.cosmwasm.com/docs/1.0/smart-contracts/testing
开发者能力矩阵 https://msgchain.org/whitepaper/developer_capability_matrix.json
合约模板索引 https://msgchain.org/whitepaper/contract_templates/index.json
release_pack 索引 https://msgchain.org/whitepaper/release_pack/index.json
交付工作流 https://msgchain.org/whitepaper/execution_pack/delivery_workflows.json
审批门禁 https://msgchain.org/whitepaper/execution_pack/approval_gates.json
治理模板 https://msgchain.org/whitepaper/execution_pack/governance_templates.json
genesis_registry_v1 https://msgchain.org/whitepaper/module_exports/registry.json

B. 术语对照

英文 中文
migrate 迁移(合约升级操作)
entry point 入口点
CW2 合约版本追踪规范
code_id 代码 ID
proxy pattern 代理模式
atomic rollback 原子回滚
reply_on 回调机制
lazy migration 懒迁移
versioned state 版本化存储
forward compatibility 向前兼容
timelock 时间锁
multisig 多签
governance proposal 治理提案
canary deployment 金丝雀部署
feature flag 功能开关
genesis_registry 创世注册中心
canonical key 规范键
fail-closed 默认失败
approval gate 审批门禁
human-in-the-loop 人在回路中

C. 常用迁移命令速查

# 查询合约当前 code_id
msg-chaind query wasm contract <addr> --node <rpc> | jq '.contract_info.code_id'

# 查询 CW2 版本信息(原始存储)
msg-chaind query wasm contract-state raw <addr> \
  636F6E74726163745F696E666F \
  --node <rpc> -o json | jq '.data' -r | base64 -d

# 上传新合约代码
msg-chaind tx wasm store <wasm_path> \
  --from <signer> --chain-id msg-chain-1 \
  --gas auto --gas-prices 1000000000attoMSG -y

# 执行迁移
msg-chaind tx wasm migrate <addr> <new_code_id> \
  '{"msg":"..."}' \
  --from <admin> --chain-id msg-chain-1 \
  --gas auto --gas-prices 1000000000attoMSG -y

# 更新注册中心 code_id 映射
msg-chaind tx wasm execute <registry_addr> \
  '{"update_code_id":{"canonical_key":"ai_agent","new_code_id":<code_id>}}' \
  --from <signer> --chain-id msg-chain-1 \
  --gas auto --gas-prices 1000000000attoMSG -y

# 提交治理迁移提案
msg-chaind tx gov submit-proposal \
  --title "升级 AI Agent" \
  --description "版本 x.y.z 迁移说明" \
  --type CosmWasmMigrateContract \
  --contract <addr> --code-id <new_code_id> \
  --msg "<base64_migrate_msg>" \
  --deposit 10000000umsg \
  --from <proposer> --chain-id msg-chain-1 -y

# 模拟迁移(预估 Gas)
msg-chaind tx wasm migrate <addr> <new_code_id> \
  '{"msg":"..."}' \
  --from <admin> --chain-id msg-chain-1 \
  --gas auto --gas-prices 1000000000attoMSG \
  --dry-run -o json | jq '.gas_estimate'

D. 迁移证据模板

{
  "schema_version": "v1",
  "migration_evidence": {
    "contract_address": "msg1ai_agent_contract...",
    "canonical_key": "ai_agent_v1",
    "from_version": "1.0.0",
    "to_version": "2.0.0",
    "from_code_id": 4,
    "to_code_id": 5,
    "migrate_tx_hash": "A1B2C3D4...",
    "migrate_block_height": 1234567,
    "migrate_timestamp": "2026-07-08T12:00:00Z",
    "migrator": "msg1multisig_address...",
    "pre_migration_snapshot": {
      "config_hash": "abc123...",
      "task_count": 150,
      "balance": "10000000umsg"
    },
    "post_migration_verification": {
      "cw2_updated": true,
      "config_readable": true,
      "tasks_migrated": 150,
      "balance_unchanged": true
    },
    "rollback_plan": {
      "exists": true,
      "target_code_id": 4,
      "rollback_msg": "eyJ2ZXJzaW9uIjoicm9sbGJhY2sifQ=="
    },
    "evidence_refs": [
      "https://explorer.msgchain.org/txs/A1B2C3D4..."
    ]
  }
}

本指南旨在帮助 AI Agent 开发者在 MSG Chain 上安全、可靠地管理合约升级与迁移。所有代码示例仅供参考,主网迁移前请务必经过完整测试、安全审计与多签批准。