IBC(Inter-Blockchain Communication) 깊게 파헤치기 — 2
들어가며
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
Subscribe to my newsletter
Read articles from Jaeseung Lee directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by