IBC를 통한 외부 CosmWasm 실행하기

2025. 4. 21. 20:58Blockchain/Smart contract

728x90
반응형

CosmWasm 컨트랙트는 Cosmos의 IBC를 통하여 서로 다른 블록체인 간의 컨트랙트 실행(execution)을 수행할 수 있다. 이번 글에서는 Golang IBC relayer를 사용하여 체인 A의 CosmWasm 컨트랙트를 실행하여서 체인 B의 CosmWasm 컨트랙트를 동작시키는 방식 환경을 구성하고 실행하는 방법을 확인한다.

환경 구성

테스트로 사용될 블록체인은 Wasm 모듈이 포함되어 있는 Juno와 Wasmd를 사용한다. 두 체인을 로컬에 설치하는데, CometBFT RPC 포트를 동일하게 26657을 사용하여, Wasmd는 docker로 ubuntu를 띄워 동작시킨다.

IBC 연결을 위한 CW 컨트랙트는 https://github.com/0xekez/cw-ibc-example에 등록된 예시 컨트랙트를 사용한다. 해당 컨트랙트를 A체인과 B체인에 동일하게 배포하고, A체인의 컨트랙트에서 실행하면 B체인의 컨트랙트 state가 변경되는 구조이다.

IBC relayer는 앞서 언급한 대로 Go-relayer를 사용한다. Go IBC relayer에 대한 설치와 동작은 해당 글에 정리를 했다. Relayer 구성 방법은 이전 글의 내용과 거의 동일하나 차이점이 존재하여 아래에서 더 설명을 할 예정이다.

컨트랙트 배포

cw-ibc-example을 각각의 체인에 배포한다. (IBC 전송을 위한 컨트랙트의 구조는 다음 기회에 확인한다.) 컨트랙트 컴파일한 .wasm 파일은 cw-ibc-example를 git clone하면 ./artifacts 폴더 내에 존재한다.

// juno

junod tx wasm store PATH/cw-ibc-example/artifacts/cw_ibc_example.wasm --from mykey1 --gas 2000000
junod tx wasm instantiate 1 '{}' --no-admin --label "local0.1.0" --from mykey1 --gas 200000


// wasmd 

wasmd tx wasm store PATH/cw-ibc-example/artifacts/cw_ibc_example.wasm --from mykey1 --gas 2000000
wasmd tx wasm instantiate 1 '{}' --no-admin --label "local0.1.0" --from mykey1 --gas 200000

각각의 컨트랙트를 instantiate한 후 확인되는 컨트랙트 주소와, 컨트랙트 정보는 아래와 같다.

// juno
juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8

// wasmd
wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d

그림 상에서 조회한 컨트랙트 정보 중 ibc_port_id를 기억해야 한다.

IBC relayer 구성

config

IBC relayer 연결을 위한 각각의 체인 구성은 아래와 같다.

// juno.json
{
  "type": "cosmos",
  "value": {
    "key": "default",
    "chain-id": "junod-1",
    "rpc-addr": "http://localhost:26657",
    "account-prefix": "juno",
    "keyring-backend": "test",
    "gas-adjustment": 1.2,
    "gas-prices": "0ujuno",
    "debug": true,
    "timeout": "20s",
    "output-format": "json",
    "sign-mode": "direct"
  }
}

// wamsd.json
{
  "type": "cosmos",
  "value": {
    "key": "default",
    "chain-id": "wasmd-1",
    "rpc-addr": "http://localhost:36657", // docker port forwarding
    "account-prefix": "wasm",
    "keyring-backend": "test",
    "gas-adjustment": 1.2,
    "gas-prices": "0stake",
    "debug": true,
    "timeout": "20s",
    "output-format": "json",
    "sign-mode": "direct"
  }
}

이 config를 이용하여 relayer를 구성하는 간단한 실행 파일을 첨부한다.

#!/bin/bash

WASMDKEY="default"
JUNOKEY="default"

WASMDINFO="./testpaths/wasmd.json"
JUNOINFO="./testpaths/juno.json"

WASMDID="wasmd-1"
JUNOID="junod-1"

PATHNAME="jwpath"

# reinstall
rm -rf ~/.relayer/
make install

# init config directory & file
rly config init --memo "test relayer"

# register chain info
rly chains add -f $WASMDINFO
rly chains add -f $JUNOINFO

# restore keys
rly keys restore wasmd $WASMDKEY "MNEMONIC WORDS" --coin-type 118
rly keys restore juno $JUNOKEY "MNEMONIC WORDS" --coin-type 118

# create connection
rly paths new $JUNOID $WASMDID $PATHNAME
rly paths list
rly chains list

IBC relayer가 양측의 체인에 트랜잭션을 전달할 수 있도록 유효한 MNEMONIC WORDS를 넣고 relayer를 세팅한다. 위 shell 파일을 실행하면 아래와 같이 확인된다.

체인에 key(X), bal(X), path(X)가 확인된다면 체인의 상태를 확인해야 한다. 만약 필자와 같이 docker로 띄웠을 경우 로컬호스트나 외부에서 docker에 접속할 수 있는 환경 구성을 미리 해야한다. 위의 예시 파일을 그대로 쓴다면 rm -rf ~/.relayer가 있으니 주의가 필요하다.

실행 후 이상이 없다면 IBC client, connection, channel을 설정해야 한다. 아래 명령을 차례로 수행하자.

rly tx clients $PATHNAME
rly tx connection $PATHNAME
rly tx channel $PATHNAME

실행이 완료되면 각각의 IBC channel ID를 확인해야 한다. 테스트를 위한 channel 생성은 Juno/Wasmd 체인 모두 channel-0이다. (각 체인의 연결된 channel ID가 다를 수 있으니 둘 다 확인해야 한다.)

이렇게 생성한 channel ID는 일반적인 IBC transfer를 위한 채널이며, IBC를 통한 CW 실행은 임의의 source/destination channel port를 설정해야 한다. 앞서 컨트랙트 배포 후 확인한 ibc_port_id를 여기서 사용한다.

rly tx channel jwpath --src-port wasm.juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8 --dst-port wasm.wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d --version counter-1

명령을 실행하면 일반 IBC channel 생성과 동일한 과정으로 채널을 생성한다. 이렇게 생성한 channel ID는 그림에서 보이는 것처럼 channel-1이며 이 채널을 사용한다. 마지막의 version flag는 컨트랙트에 const로 박혀있는 버전이다.

이후 relayer를 실행한다.

rly start

컨트랙트 실행

컨트랙트 구조

Juno에 배포되어 있는 컨트랙트를 이용하여 Wasmd의 state를 변경한다. 예시로 사용된 cw-ibc-example은 한쪽 체인에서 실행하면 다른쪽 체인의 counter가 증가하는 구조이다. 이 컨트랙트의 실행 메시지 구조는 아래와 같다.

#[cw_serde]
pub struct InstantiateMsg {}

#[cw_serde]
pub enum ExecuteMsg {
    Increment { channel: String },
}

#[cw_serde]
pub enum IbcExecuteMsg {
    Increment {},
}

#[cw_serde]
#[derive(QueryResponses)]
pub enum QueryMsg {
    // GetCount returns the current count as a json-encoded number
    #[returns(crate::msg::GetCountResponse)]
    GetCount {
        // The ID of the LOCAL channel you'd like to query the count
        // for.
        channel: String,
    },
    // GetTimeoutCount returns the number of timeouts have occured on
    // the LOCAL channel `channel`.
    #[returns(crate::msg::GetCountResponse)]
    GetTimeoutCount { channel: String },
}

// We define a custom struct for each query response
#[cw_serde]
pub struct GetCountResponse {
    pub count: u32,
}

해당 컨트랙트의 IBC로 실행되는 함수 구조를 간단히 살펴본다.


#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    _deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Increment { channel } => Ok(Response::new()
            .add_attribute("method", "execute_increment")
            .add_attribute("channel", channel.clone())
            // outbound IBC message, where packet is then received on other chain
            .add_message(IbcMsg::SendPacket {
                channel_id: channel,
                data: to_binary(&IbcExecuteMsg::Increment {})?,
                // default timeout of two minutes.
                timeout: IbcTimeout::with_timestamp(env.block.time.plus_seconds(120)),
            })),
    }
}

먼저 위는 execute 함수이다. ExecuteMsg::Increment가 컨트랙트로 인입되면 Responseadd_message를 통해 IbcMsg::SendPacket이 실행된다.


// cosmwasm-std-1.2.2/src/ibc.rs


/// Sends an IBC packet with given data over the existing channel.
/// Data should be encoded in a format defined by the channel version,
/// and the module on the other side should know how to parse this.
SendPacket {
    channel_id: String,
    data: Binary,
    /// when packet times out, measured on remote chain
    timeout: IbcTimeout,
},

IbcMsg의 구조는 위와 같으며, IBC 전송을 위한 channel ID, IBC tx의 timeout이 입력되며, binary 타입으로 IbcExecuteMsg::Increment{}data가 전달된다.

IBC transaction이 전달되고, 채널의 ibc_port_id를 통해 컨트랙트에 IBC packet이 인입된다. 해당 인입된 패킷 처리는 cw-ibc-example에서 아래와 같이 처리된다.


#[cfg_attr(not(feature = "library"), entry_point)]
pub fn ibc_packet_receive(
    deps: DepsMut,
    env: Env,
    msg: IbcPacketReceiveMsg,
) -> Result<IbcReceiveResponse, Never> {
    // Regardless of if our processing of this packet works we need to
    // commit an ACK to the chain. As such, we wrap all handling logic
    // in a seprate function and on error write out an error ack.
    match do_ibc_packet_receive(deps, env, msg) {
        Ok(response) => Ok(response),
        Err(error) => Ok(IbcReceiveResponse::new()
            .add_attribute("method", "ibc_packet_receive")
            .add_attribute("error", error.to_string())
            .set_ack(make_ack_fail(error.to_string()))),
    }
}

pub fn do_ibc_packet_receive(
    deps: DepsMut,
    _env: Env,
    msg: IbcPacketReceiveMsg,
) -> Result<IbcReceiveResponse, ContractError> {
    // The channel this packet is being relayed along on this chain.
    let channel = msg.packet.dest.channel_id;
    let msg: IbcExecuteMsg = from_binary(&msg.packet.data)?;

    match msg {
        IbcExecuteMsg::Increment {} => execute_increment(deps, channel),
    }
}

fn execute_increment(deps: DepsMut, channel: String) -> Result<IbcReceiveResponse, ContractError> {
    let count = try_increment(deps, channel)?;
    Ok(IbcReceiveResponse::new()
        .add_attribute("method", "execute_increment")
        .add_attribute("count", count.to_string())
        .set_ack(make_ack_success()))
}

Binary를 다시 plain message로 변환하고, 변환된 datamatch를 통해 IbcExecuteMsg::Increment로 확인되면 execute_increment() 함수를 실행한다. count를 증가시키고, IbcReceiveResponseset_ack()를 통해 IBC ack를 전송한다.

CLI를 통한 컨트랙트 실행

먼저 wasmd의 컨트랙트에 query를 통해 현재 count를 확인한다. 아래 CLI query 시 IBC relayer로 양측 체인에 연결된 channel ID를 사용한다.


wasmd query wasm contract-state smart wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d '{"get\_count": {"channel": "channel-1"}}'

그림과 같이 현재 count는 0으로 확인된다.

이제 Juno에서 컨트랙트를 실행한다.


junod tx wasm execute juno14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9skjuwg8 '{"increment": { "channel": "channel-1" }}' --from mykey1 --gas 200000

CLI로 명령을 수행하면 relayer에서는 IBC transaction을 전송한다.

동일하게 get_count query를 수행하면 Wasmd에 배포된 컨트랙트의 count가 1 증가한 것을 확인할 수 있다.

Conclusion

CosmWasm 컨트랙트 실행을 통해 IBC로 외부 체인에 배포된 다른 CosmWasm 컨트랙트를 실행하는 방법을 확인해보았다. 이렇게 구현하기 위해서는 컨트랙트 내부에 IBC를 처리할 수 있게 하는 코드가 추가되어야 한다. 예시로 본 cw-ibc-example에서는 src/ibc.rs에 IBC handshake와 IBC 메시지를 받기 위한 아래와 같은 기능들을 구현해 놓았다.

  • ibc_channel_open - Handles the OpenInit and OpenTry handshake steps.
  • ibc_channel_connect - Handles the OpenAck and OpenConfirm handshake steps.
  • ibc_channel_close - Handles the closing of an IBC channel by the counterparty.
  • ibc_packet_receive - Handles receiving IBC packets from the counterparty.
  • ibc_packet_ack - Handles ACK messages from the countarparty. This is effectively identical to the ACK message type in TCP.
  • ibc_packet_timeout - Handles packet timeouts.

이를 응용해서 inter-chain 간의 다양한 비즈니스를 구축할 수 있을 것이다.

참고
https://github.com/0xekez/cw-ibc-example

728x90
반응형