MSG Chain AI Agent 合约升级与迁移模式指南
适用对象
本指南面向需要在 MSG Chain(msg-chain-1)上管理 CosmWasm 智能合约生命周期升级的 AI Agent 开发者、合约工程师与基础设施运维团队。
⚠️ No-Go Disclaimer: MSGChain 主网裁决为 No-Go。本文件所有内容反映的是开发阶段的技术设计,不代表主网未独立核验上线状态。生产部署状态请以白皮书为准:https://msgchain.org/whitepaper/
目录
- 引言
- CosmWasm 升级机制
- 迁移生命周期
- 数据迁移策略
- 代理模式(Proxy Pattern)
- 自动迁移治理
- 回滚与应急回退
- 升级测试
- AI Agent 升级工作流
- 实战:AI Agent 合约版本演进案例
- 安全注意事项
- 总结
1. 引言
1.1 不可变性 vs 可升级性
智能合约部署到区块链后,其字节码在链上不可篡改。这是区块链信任的基础——用户能够验证他们交互的代码与审计过的代码完全一致。然而,这种不可变性也给软件演进带来了根本性矛盾:
- Bug 修复:发现安全漏洞后无法直接修改已部署合约
- 功能迭代:无法新增特性或调整业务逻辑
- 状态迁移:旧合约中的资金和数据无法自动转移到新逻辑
- 参数调整:无法根据市场条件调整费率、阈值等配置参数
CosmWasm 通过 migrate 入口点提供了受控的可升级机制,在不牺牲不可变性的前提下,允许合约从一个 code_id 迁移到另一个 code_id。这是 Cosmos 生态相较于以太坊 Solidity 合约的一个关键优势——后者通常需要依赖代理模式或不可变合约配合新部署来实现升级。
1.2 AI Agent 的演进需求
在 MSG Chain 上运行的 AI Agent 合约面临比传统 DeFi 合约更频繁的演进需求:
- 策略更新:AI Agent 的决策模型、评分算法或优化策略需要定期迭代
- 数据格式变更:Agent 积累的链上信誉数据、交互记录的结构可能随版本演进而变化
- 通信协议升级:Agent 之间的消息格式、路由方式可能随生态演化而调整
- 经济模型调整:激励分配、费率结构、质押参数需要根据实际运行数据校准
- 合规与治理:DAO 投票通过的规则变更需要反映到 Agent 的执行逻辑中
这些需求使得合约升级成为 AI Agent 生命周期管理的核心能力。
1.3 MSG Chain 升级基础能力
MSG Chain(msg-chain-1)的 CosmWasm 运行时提供以下升级基础设施:
- wasm migrate:合约迁移入口点,由 CosmWasm 虚拟机原生支持
- reply on:子消息执行后的回调机制,用于原子性检查与条件提交
- 原子回滚:在迁移过程中如果
migrate返回错误,整个交易自动回滚 - CW2 版本追踪:链上记录合约的合约名与版本号
- genesis_registry_v1:注册中心记录合约的 canonical key 与当前 code_id 映射
- 多签支持:主网合约必须由多签地址管理,升级需要多方签名
本指南将深入探讨这些机制的工作原理、最佳实践以及在 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 接收一个已初始化的存储(已有状态)。它必须:
- 验证调用者是否有权执行迁移
- 从旧状态中读取数据
- 根据
MigrateMsg中的指令执行数据转换 - 将转换后的数据写入新存储结构
- 更新 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>,
}
设计原则:
- 向前兼容:
MigrateMsg的字段应尽量保留旧合约能理解的默认值 - 最小参数:迁移消息不应包含不必要的参数,减少治理负担
- 自描述:
version字段让新合约能判断该消息是为哪个版本设计的
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 对应的:
- contract_address:合约实例地址
- code_id:当前关联的代码 ID
- cw2_version:从合约存储中读取的 CW2 版本信息
- migration_history:迁移历史记录(code_id 变更序列)
这种双层记录(合约内 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 状态迁移的挑战
与传统的数据库迁移不同,链上状态迁移面临以下约束:
- 不可逆性:一旦提交,旧状态结构被覆盖无法恢复
- Gas 限制:迁移逻辑必须在单个交易的 Gas 限额内完成
- 原子性:迁移要么全部成功,要么全部回滚(CosmWasm 的原子回滚保证)
- 存储 key 冲突:新旧版本的存储 key 可能重叠,导致数据损坏
- 查询兼容性:迁移期间及之后,查询接口可能需要保持向后兼容
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 什么是代理模式
代理模式是一种合约架构,其中存在两个合约:
- 代理合约(Proxy):持有状态和资产,接收所有用户请求,通过消息转发将执行委托给逻辑合约
- 逻辑合约(Logic/Implementation):包含业务逻辑,不持有持久状态
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 回滚的必要性
常见故障场景:
- 状态损坏:迁移逻辑中存在未覆盖的边界条件
- 性能退化:新逻辑引入未预期的 Gas 消耗增加
- 逻辑错误:新代码中存在未发现的 bug
- 兼容性问题:与其他合约的交互模式被改变
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
回滚合约的关键问题:
- 旧版本的 migrate 入口点需要能处理回滚消息
- 如果新版本修改了存储结构,回滚时需要转回旧格式
- 回滚后 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)),
}
}
原子回滚保证:
- 如果
migrate返回Err,整个交易回滚 - 如果
SubMsg迁移失败,根据reply_on策略决定回滚 - MSG Chain 运行时确保了这种原子性
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 为什么需要在测试环境中测试迁移
合约迁移涉及状态转换,测试覆盖度直接影响主网安全性。测试目标:
- 功能正确性:迁移后的合约功能与预期一致
- 数据完整性:所有旧状态被正确转换为新格式
- Gas 可行性:迁移交易在 Gas 限额内可完成
- 权限正确性:只有授权账户可以触发迁移
- 回滚可行性:回滚后合约功能恢复正常
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 {});
}
}
安全建议:
- 始终同时检查
info.sender和合约 admin - 不要依赖
env中的信息做权限判断 - 考虑引入多签检查
- 对于 AI Agent,建议迁移权限与控制权限分离
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 升级最佳实践
- 始终在 Testnet 验证迁移:稳定运行至少 72 小时
- 始终准备回滚计划:每个升级必须有对应的回滚方案
- 使用 CW2 标准版本追踪:永远不要绕过 cw2::set_contract_version
- 存储 key 版本化:使用带版本号的前缀,避免 key 冲突
- 保持查询向前兼容:至少在当前版本和上一个版本之间保持兼容
- 权限分离:迁移权限、管理权限、操作权限应分离
- Gas 预算管理:迁移逻辑应在 5M gas 内完成
- 原子回滚保障:利用 CosmWasm 的原子回滚特性
- 注册中心同步:迁移后更新 genesis_registry_v1 中的 code_id 映射
- 监控持续:迁移后保持至少 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 上安全、可靠地管理合约升级与迁移。所有代码示例仅供参考,主网迁移前请务必经过完整测试、安全审计与多签批准。
