CosmWasm Contract Migration

2023. 6. 15. 10:15Blockchain/Smart contract

728x90
반응형

※ 원글 작성 : 22년 11월 24일

Migration?

Contract를 통한 서비스를 운영하다 보면 버그가 발견거나, 기능이 업데이트 되야 하거나 하는 이슈로 인해 contract를 수정해야만 하는 상황이 발생할 수도 있다. 이때 기존 contract의 state는 유지하면서 contract 코드만 수정하는 방법이 "migration"이다. CosmWasm은 이런 migration이 굉장히 편하고 쉽게 되어 있다. Wasm 모듈이 포함되어 있는 cosmos sdk 기반 블록체인에서 이 migrate를 지원하니, contract 운영이 쉬워질 것이라 생각한다.

Migration code

Migration을 하기 위해서는 우선 CW contract 내에 contract version이 관리되고 있어야 한다. CosmWasm의 docs를 확인하면

const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");


#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(deps: DepsMut, env: Env, info: MessageInfo, msg: InstantiateMsg) -> Response {
    // Use CW2 to set the contract version, this is needed for migrations
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
}

처럼 contract name과 contract version을 instantiate function에 추가해야 한다. 이 contract version을 통해 migration 시 정보를 참고한다. 위에서 확인되는 CARGO_PKG_NAMEGARGO_PKG_VERSION은 Cargo.toml 파일 내에서 contract 생성 시에 설정이 필요하다.

// Cargo.toml

[package]
name = "cosmwasm-contract-name"
version = "0.1.0"

Migration을 할 새로운 코드 상에는 entry_point로 migrate function이 추가되어야 한다. 가장 기본적인 migrate function은 아래와 같으며 이런 형식의 function은 특별한 제약조건 없이 migration 될 수 있다.

#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    // No state migrations performed, just returned a Response
    Ok(Response::default())
}

보통 MigrateMsg는 empty set을 사용하며, custom 할 수 있다.

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}

하지만 contract name을 잘못 설정한다거나, contract version이 이전 버전 코드를 그대로 사용할 수 있는 오류를 범할 수 있기 때문에 CosmWasm에서는 아까 설정했던 set_contract_version에 기록된 params를 활용하여 migrate에 대한 제약 조건을 설정할 수 있다.

#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
    let ver = cw2::get_contract_version(deps.storage)?;
    // ensure we are migrating from an allowed contract
    if ver.contract != CONTRACT_NAME {
        return Err(StdError::generic_err("Can only upgrade from same type").into());
    }
    // note: better to do proper semver compare, but string compare *usually* works
    #[allow(clippy::cmp_owned)]
    if ver.version >= CONTRACT_VERSION {
        return Err(StdError::generic_err("Cannot upgrade from a newer version").into());
    }

    // set the new version
    cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    // do any desired state migrations...

    Ok(Response::default())
}

이러한 식으로 설정을 한다면, migration 시에 같은 contract name의 contract이면서 새로운 버전의 contract version인 CW contract만 migration을 진행 할 수 있다. 즉, CARGO_PKG_NAME은 같으면서, CARGO_PKG_VERSION은 큰 contract를 사용해야 한다.

// Cargo.toml

[package]
name = "cosmwasm-contract-name"
version = "0.1.1"

Wasm module 동작

먼저 기존 0.1.0 cw 코드를 store 후 확인되는 code ID로 instantiate를 해야 한다. Instantiate 때 contract admin option을 할당 하지 않거나, 잘못된 주소를 입력해버리면 can not migrate: unauthorized의 error를 반환할 수 있으니, contract owner는 주의해서 admin을 설정해야 한다. query wasm contract [contract-address]를 사용하면 contract의 admin을 확인 할 수 있다.

여러 execute를 진행하며 state 기록이 많이 쌓인 후 migration이 필요할 때가 왔을 때 앞서 언급한 방식대로 새로운 코드(0.1.1)를 준비한다. 그리고, migrate 동작을 수행한다.

Wasmd를 보면 "migrate [contract_addr_bech32] [new_code_id_int64] [json_encoded_migration_args]"를 보면서 어떤 메시지를 보내야 하는지 확인할 수 있다. Contract address는 기존 contract 것을 사용하며, new code ID는 새로운 contract를 store한 뒤 생성되는 code ID를 사용해야한다. 즉, 새로운 contract는 instantiate까지 하지 말고 store만 진행한 후 code ID를 받아놓은 상태에서 멈춰야 한다. Encoded migration args는 json 형식을 mashaling한 encoding data를 사용해야 한다. MigrateMsg가 empty set일 때 golang을 예로 들면,

type MigrateMsg struct {}

...

var migrateMsg MigrateMsg
byteData, err := json.Marshal(migrateMsg)

로 생성되는 []byte 형식의 데이터를 string으로 변환 후 사용하면 된다. 이후 migrate를 tx를 전송(tx wasm migrate)하면, migration이 완료된다.

Implementation

기존 코드(0.1.0)를 store, instantiate 한 후 query wasm contract 명령을 통해 contract info를 확인하면 다음과 같이 확인할 수 있다. 그림의 Contract address에 대한 code ID 363번인 것을 가능하다.

이 때 364번의 새로운 코드(0.1.1)를 store 한 후 code ID를 확인해 놓고,

migration을 진행하면 아래와 같은 log를 확인 할 수 있다.

contract-history도 확인해 보면 363의 code ID에서 364의 code ID로 변경된 것을 확인할 수 있다.

이제부터 동일한 contract address가 새로운 코드의 로직을 따를것이다.

참고
https://docs.cosmwasm.com/docs/1.0/smart-contracts/migration/

728x90
반응형