IBC(Inter-Blockchain Communication) 깊게 파헤치기 — 2

Jaeseung LeeJaeseung Lee
4 min read

들어가며

1편에서는 IBC에 대한 전체적인 개요와 IBC가 trustless한 체인간 통신을 지원하기 위한 필수 구성요소인 IBC Client, Connection, Channel의 개념과 실제 구현체를 살펴보았다. 2편에서는 양 체인에서 IBC Client, Connection, Channel 설정된 이후 실제 IBC 패킷이 어떻게 이동하고 검증하는 지 자세하게 알아보도록 하겠다.

Packet Flow

Send Packet (Source side)

1편에서 Port는 특정 IBC Application과 연결된다고 설명했다. 예를 들어 trasnfer라는 Port는 bank 모듈에서 생성되는 토큰을 체인간 전송하는 로직과 연결되어 있다. 이런 다양한 IBC Application 로직들이 각자 필요한 동작을 보내는 체인 측에서 실행하고 마지막으로는 IBC Packet 형태로 데이터를 변환하고 전송하는 작업을 진행한다.

IBC Packet을 처리하는 과정을 좀 더 자세하게 살펴보면 이 때 우선 source channel ID와 source port 값을 통해 보내는 체인 측의 channel 정보를 불러오고 이 채널이 OPEN 상태인지 확인한다. 이후 SendPacket을 호출한 IBC App 모듈이 해당 channel, port에서 권한을 가지는 지 capability를 검사한다. 앞의 과정을 통과하면 IBC App에서 처리하기 위한 데이터를 마샬링한 []byte, Packet sequence, Source/Destination port, Source/Destination channel, TimeoutHeight, TimeoutTimestamp 값을 포함한 IBC Packet을 생성한다.

// Custom packet data defined in application module
type CustomPacketData struct {
    // Custom fields ...
}

func EncodePacketData(packetData CustomPacketData) []byte {
    // encode packetData to bytes
}

func DecodePacketData(encoded []byte) CustomPacketData {
    // decode from bytes to packet data
}

Packet 생성 이후에는 Channel ID에 연동된 IBC Client가 Active 상태인지 확인 후 ClientState의 timestamp와 block height가 packet의 TimeoutTimestamp, TimeoutHeight을 넘지 않았는지 검증한다. 마지막으로는 TimeoutTimestamp, TimeoutHeight, Packet sequence, packet date bytes값을 serialization한 후 SHA256 해싱해서 Packet Commitment를 생성하고 이를 source PortID/ChannelID/PacketSequence를 key로 해서 저장한다.

Receive Packet (Destination side)

IBC 보내는 체인 측에서 앞선 과정을 진행하면 send_packet 이벤트가 발생한다. IBC Relayer는 해당 이벤트 정보를 본 후 이를 기반으로 MsgRecvPacket 를 만들어 destination 체인에 전송한다. 상대 체인에서도 역시 Destination portID/channelID를 통해 저장된 channel을 가져온 후 1)OPEN 상태인지, 2)channel의 Counterparty portID/channelID 정보가 packet의 source portID/channelID 정보와 일치하는지, 3)해당 channel과 연결된 connection이 OPEN인지 확인한다. 이후 해당 체인의 BlockHeight, BlockTime이 packet에서 설정된 TimeoutTimestamp, TimeoutHeight보다 이전인지 검증하고 통과하면 전달된 packet에 대한 commitment를 생성해서 이것과 동일한 것이 보내는 체인 측에 저장되었는지 proof와 함께 확인한다. 이 때 해당 proof는 abci_query를 통해 보내는 체인 쪽에서 가져올 수 있다.

proof를 통한 검증이 끝나면 UNORDERED 패킷의 경우 처리 후 PacketReceipt를 만들어서 저장하고 ORDERED 패킷의 nextSequenceRecv가 packet.sequence와 동일한 지 검증하고 처리한다. 마지막으로는 OnRecvPacket을 타겟 IBC App에서 실행시켜 관련 콜백 로직을 처리하고 PacketAcknowledgement를 저장한다. 이 때 key는 destination PortID/ChannelID/PacketSequence이다.

Acknowledge Packet (Source side)

앞 과정을 통해 recv_packet이 발생하면 IBC Relayer는 해당 이벤트 정보를 본 후 이를 기반으로 MsgAcknowledgement를 만들어 source chain에 전송한다. 이후 RecvPacket 과정처럼 channel, connection, capability 검증을 진행한 후 SendPacket에서 저장된 PacketCommitment가 전달된 Packet으로 만들어진 Commitment와 동일한 지 검증. 이후 destination chain에 올바르게 Acknowledgement가 기록되었는지 proof를 통해 검증을 진행한다. 그다음 ORDERED 채널인 경우 nextSequenceAck이 해당 packet sequence와 동일한지 비교하고 source chain에 저장된 packet commitment를 제거한다. 마지막으로는 OnAcknowledgementPacket을 타겟 IBC App에서 실행시켜 관련 콜백 로직을 처리한다.

Timeout Packet (Source side)

종종 IBC 패킷 전송 시 설정한 TimeoutTimestamp, TimeoutHeight 안에 destination chain에 전달하지 못하는 상황이 발생할 수 있다. 이 때 source chain에서 해당 IBC 패킷에 대해 timeout 처리를 할 수 있다. TimeoutTimestamp이나TimeoutHeight 둘 중 하나의 조건에만 만족해도 해당 패킷은 timeout이다.

Acknowledgement처럼 channel, connection, capability 검증을 진행한 후timeout을 입증할 BlockHeight를 정하고 이 height에 해당하는 timestamp를 ClientState를 통해 불러온다. 이후 packet의 TimeoutTimestamp, TimeoutHeight보다 이 값들이 큰 지 비교해보고 packet commitment가 인자로 전달된 packet과 일치하는지 확인한다. 그다음 ORDERED 채널인 경우 nextSequenceRecv가 destination chain에서 증가되지 않고 그대로인지, UNORDERED 채널인 경우 PacketReceipt가 존재하지 않는 다는 사실을 proof를 통해 검증한다. 마지막으로는 OnTimeoutPacket을 타겟 IBC App에서 실행시켜 관련 콜백 로직을 처리하고 SendPacket에서 저장했던 PacketCommitment를 제거한다. 이 때 주의할 점은 ORDERED 채널의 경우 한번 timeout이 발생하면 그 채널은**CLOSED**상태가 되고 다시는 복구할 수 없다. CLOSED가 된 채널에 보냈던 packet들은 TimeoutOnClose를 통해 역시 timeout 처리할 수 있다.

IBC Middleware

IBC가 처음 설계될 당시에는 IBC Packet 자체에 대한 처리 순서 설정, 검증, 송수신 등 핵심 공통 로직은 core에 구현되고 IBC 관련된 App 로직(e.g. IBC bank transfer)은 apps에서 각각의 모듈로 구현되었다. 이후 core에는 포함되지 않지만 여러 IBC App 로직에서 공통으로 사용하게 되는 로직에 대한 필요성이 발생했고 IBC middleware는 그것을 위한 도구이다.

Overview of a middleware stack (ref. ibc-go docs)

Middleware 인터페이스를 구현하면 IBC middleware로써 동작할 수 있다.

type IBCModule interface {
    OnChanOpenInit(...) (string, error)
    OnChanOpenTry(...) (version string, err error)
    OnChanOpenAck(...) error
    OnChanOpenConfirm(...) error
    OnChanCloseInit(...) error
    OnChanCloseConfirm(...) error
    OnRecvPacket(...) exported.Acknowledgement
    OnAcknowledgementPacket(...) error
    OnTimeoutPacket(...) error
}

type ICS4Wrapper interface {
    SendPacket(...) (sequence uint64, err error)
    WriteAcknowledgement(...) error
    GetAppVersion(...) (string, bool)
}

type Middleware interface {
    IBCModule
    ICS4Wrapper
}

var _ porttypes.Middleware = (*IBCMiddleware)(nil)

func NewIBCMiddleware(app porttypes.IBCModule, k keeper.Keeper) IBCMiddleware {
    return IBCMiddleware{
        app:    app,
        keeper: k,
    }
}

아래는 IBC transfer 모듈에서 middleware를 적용한 코드이다. 콜백로직 중 하나인 OnRecvPacket을 처리하는 과정을 따라가보면 core.RecvPacket -> fee.OnRecvPacket -> transfer.OnRecvPacket이 되고 ICS4Wrapper 로직인 SendPacket을 과정을 따라가보면 transfer.SendPacket -> fee.SendPacket -> core(channelKeeper).SendPacket이 된다. 처음 소개되었던 IBC middleware 다이어그램의 흐름을 그대로 따라감을 알 수 있다.

var transferStack porttypes.IBCModule
transferStack = transfer.NewIBCModule(app.TransferKeeper)
transferStack = ibcfee.NewIBCMiddleware(transferStack, app.IBCFeeKeeper)
ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack)

이번 글에서는 실제 core 모듈단에서 채널 종류와 패킷 전송 설정에 따라 어떻게 IBC 패킷이 처리되는지 단계별로 알아보았다. 다음 글에서는 실제 구현된 IBC App들의 종류와 그 동작을 자세히 알아보겠다.

References

0
Subscribe to my newsletter

Read articles from Jaeseung Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jaeseung Lee
Jaeseung Lee