IBC(Inter-Blockchain Communication) 깊게 파헤치기 — 1
들어가며
DeFi, NFT 등 DApp에 많은 관심이 쏠리며 상대적으로 덜 조명받았었던 메인넷 분야가 최근 다시 시장을 뜨겁게 달구고 있다. 이더리움 기반의 Layer2인 Optimism, Arbitrum이 각각 Bedrock, Nitro로 작년에 대규모 업데이트를 진행하며 다양한 DApp들이 가스비가 비싼 이더리움을 벗어나 L2에서 다시 런칭하기 시작했고 FTX 붕괴 이후 주춤했던 Solana의 재도약, 모듈러 블록체인의 원조 Celestia, MoveVM의 Aptos, Sui, 1초도 안되는 합의 시간으로 급부상하는 Sei 등 각 사용자 니즈에 맞춰서 성장하고 있는 멀티체인 생태계는 선택이 아닌 필수가 되었다.
하지만 이렇게 다양한 옵션을 제공하는 멀티체인 생태계가 장점만 있는 것은 아니다. 블록체인은 기본적으로 네트워크를 이루는 노드간 합의를 통해 현재 상태에서 다음 상태로 상태 변경을 일으키는데 이러한 구조로 인해 기존 Web2처럼 단순히 E2E 암호화 통신으로 서로의 데이터를 교환할 수 없다. 즉, 특정 메인넷 입장에서 해당 메인넷 위의 데이터가 아닌 다른 메인넷의 데이터는 외부 데이터의 하나일 뿐이고 직접 검증을 하지 못한 믿을 수 없는 데이터인 것이다. 따라서 A 메인넷의 토큰을 B 메인넷으로 옮기려고 해도 A 메인넷의 관점에서는 이 토큰이 정말 B 메인넷에 존재는 하는지, 존재한다면 보내는 사람이 진짜 해당 잔고만큼의 토큰을 가지고 있는지, 가지고 있다면 진짜 보낸 수량만큼 B 메인넷에서 그 상태를 반영했는지 믿을 수가 없는 것이다.
이 문제를 해결하기 위해 브릿지라는 별도의 중개자가 필요하다. 브릿지를 구현하는 방법은 여러가지가 있지만 핵심은 제3자의 관점으로 토큰을 주고 받을 양 체인의 상태를 검증하고 처리하는 것이다. Wormhole같은 컨트랙트 기반의 중앙화된 브릿지의 경우 대부분 Multisig를 기반으로 체인간 토큰 이동을 검증하고 승인하는 형태이다. 따라서 탈중앙화된 형태로 검증되지 못하고 해당 Multisig 키가 탈취되면 브릿지 자체가 해킹당하는 상황이 발생한다. 또한, 대부분 브릿지 자체에서 미리 유동성을 양쪽 체인에 준비해두고 사용자가 전송 시 Lock & Mint 형식으로 진행되기 때문에 한번 해킹당할 때 피해액이 클 가능성이 높다. 2024년 1월까지 해킹당한 브릿지는 ChainSwap, Multichain, Thorchain, Poly Network, Wormhole, Ronin, Harmony, Nomad, BNB, Orbit 등이고 대부분 스마트 컨트랙트 버그이거나 키 유출로 인해 발생했고 총 피해액은 무려 27조에 달한다. 이런 중앙화 브릿지의 리스크를 줄이기 위해 최근 Axelar나 LayerZero처럼 브릿지를 위한 별도의 네트워크를 운영해서 이를 중개하는 형태의 구조도 있는데 이 역시 보내는 체인, 받는 체인의 보안성이 얼마나 높은지와는 별개로 중개하는 해당 네트워크의 보안에 따라 (e.g. 비잔틴에 의한 잘못된 합의) 중앙화된 브릿지와 동일한 문제가 발생할 수 있다. 그렇다면 이 브릿지의 딜레마는 정말 벗어날 수 없는걸까?
IBC란?
IBC는 Inter-Blockchain Communication Protocol의 약자로 코스모스 생태계에서 블록체인간 통신을 프로토콜 레벨로 정의한 구현체이다. 코스모스의 경우 지금은 CosmWasm 같은 스마트 컨트랙트를 위한 VM이 존재하지만 태생이 앱체인이기 때문에 개발 초창기부터 프로토콜 레벨에서 체인간 통신하는 기술에 많은 시간을 투자하였다. 그 결과 앞서 소개된 브릿지 솔루션들과는 달리 중앙화된 주체를 믿거나 제3의 네트워크에 의존하지 않고 상대 체인의 합의 알고리즘이 정상적으로 작동하는한 문제 없는 탈중앙화된 브릿지 프로토콜이 탄생하였다. IBC의 경우 내가 A 체인에서 B 체인으로 송금한다고 가정하면 A->B로 얼마를 보냈다는 증거를 A 체인에 남긴다. 이 때 증거를 남기기 위해 트랜잭션을 전파되고 여기서 발생하는 이벤트를 Relayer가 감지하여 B 체인으로 중개해준다. B 체인에서는 중개된 데이터를 받으면 이를 받았다는 증거를 B 체인에 남기고 관련 이벤트를 감지한 릴레이어가 해당 결과를 다시 A로 중개해줌으로써 IBC 패킷 교환은 종료된다. 이 설명은 IBC의 큰 그림을 보여주기 위해 최대한 기술적인 부문을 배제한 것이고 구체적인 구현 부문은 이어질 내용에서 다뤄볼 것이다.
IBC는 현재 ICS(Interchain Standards)라는 표준을 기준으로 구현이 진행되고 패킷 교환에 핵심이 되는 Core 부문은 Client(ICS-2, 6, 7, 8, 9, 10), Connection(ICS-3), Channel( ICS-4), Port(ICS-5), Vector Commitment(ICS-23), Relayer(ICS-18)이다. 이후 자세히 다룰 것이지만 지금 각 요소를 간단히 설명해보면 다음과 같다. Client는 상대 체인의 상태를 추적하는 모듈화된 구현체이다. 즉, 별도의 프로세스가 아니라 체인 안의 하나의 모듈로써 동작하고 각 상대 체인별로 추상화된 Client를 내부에 생성하게 된다. 이 때 이 Client의 상태 업데이트는 중개 노드인 Relayer가 진행한다. Connection의 경우 IBC 통신을 진행할 때 서로 검증을 원할하게 할 수 있게 IBC version, 상대방의 merkle path prefix 등을 저장하는 역할을 한다. Channel의 경우 정보의 종류를 구분하는 역할을 한다. 예를 들어 A->B로 Y토큰을 보내는 것과 Z토큰을 보내는 것은 IBC 관점에서는 다른 Channel을 사용해야 한다. 이밖에도 보내는 체인에서 패킷이 생성된 순서대로 처리할 것인지(ORDERED) 받는 체인에서 패킷이 수신되는 순서대로 처리할 것인지(UNORDERED) 정할 수도 있다. Port의 경우 IBC 통신을 통해 보내지는 마샬링된 패킷이 상대 체인에서 어떻게 처리되야 할지 알려주는 인자이다. 즉, 이 Port에 맞춰서 Application 부문에서 적절한 핸들러를 구현한다.
IBC Application은 추상화된 IBC Core 위에서 실제 필요한 로직들을 실행하는 레이어이다. 올바르게 IBC Application을 구현하는 조건은 [IBCModule](https://github.com/cosmos/ibc-go/blob/v8.0.0/modules/core/05-port/types/module.go#L14-L107)
인터페이스를 따르면 된다. 이는 Channel Open, Close 과정에서 발생하는 각 단계 및 상대방 패킷을 받을 때(recv, ack), 패킷이 만료될 때(timeout) 적절한 콜백함수를 구현하는 것이다. 또한, IBC Application과 IBC Core 사이에 추가적인 로직을 넣기 위해 구현하는 IBC Middleware의 경우 IBCModule
인터페이스와 더불어 [ICS4Wrapper](https://github.com/cosmos/ibc-go/blob/v8.0.0/modules/core/05-port/types/module.go#L110-L133)
인터페이스를 구현해야 하고 이것이 구현되면 패킷을 보내거나 받을 때 중간에 가로채서 추가적인 동작을 실행할 수 있다.
Vector Commitment
Vector Commitment는 앞에서 설명한 증거를 남기는데 사용되는 ICS-23 기반의 구조이다. IBC는 Client에 상대 체인의 특정 높이에 대한 상태가 업데이트되면 그 상태를 기반으로 ICS-24에서 정해진 merkle path에서 merkle proof를 통해 Relayer가 전송한 증명과 예상하는 상태가 일치하는지 검증한다. 단, Client 업데이트 이후 해당 패킷이 보내진 Connection에 설정된 DelayPeriod
가 지나기 전에 검증 요청을 하는 경우 거부된다. ICS-23은 특정 값이 특정 상태에서 존재하는지 검증하는 VectorMembership
과 특정 값이 특정 상태에서 존재하지 않는다는 것을 검증하는 VectorNonMembership
두가지 메서드를 제공한다.
Core
Client
Client는 IBC 패킷 검증을 위해 상대 체인의 정보를 저장하고 있는 객체이다. 이론적으로 [ClientState](https://github.com/cosmos/ibc-go/blob/2551dea41cd3c512845007ca895c8402afa9b79f/modules/core/exported/client.go#L48)
인터페이스와 [ConsensusState](https://github.com/cosmos/ibc-go/blob/2551dea41cd3c512845007ca895c8402afa9b79f/modules/core/exported/client.go#L145)
인터페이스를 만족시켜서 구현하면 어떤 체인이라도 IBC와 호환되는 Client 객체를 만들 수 있다. 현재 IBC v8 기준으로는 코스모스 계열의 합의 알고리즘인 Tendermint에 대응하는 07-tendermint와 체인 상태를 가지고 있지 않으면서 다른 체인들과 상호작용하는 단독 클라이언트인 06-solomachine, E2E 테스트, 시뮬레이션 등에 사용되는 09-localhost가 구현되어 있다. Polkadot이 사용하는 GRANDPA 알고리즘과 연동되는 클라이언트는 현재 개발을 진행 중에 있다.
IBC 연결을 위해 상대 체인의 상태를 추적하는 Client를 생성하기 위해서는 우선 CreateClient 메시지를 내 체인에 보내야 한다. 해당 메시지는 ClientState
와 ConsensusState
를 포함하고 있다.
message MsgCreateClient {
google.protobuf.Any client_state = 1;
google.protobuf.Any consensus_state = 2;
string signer = 3;
}
각 State는 해당 클라리언트 타입에 맞춰 각자 구현되기 때문에 여기서는 가장 범용적인 07-tendermint를 기준으로 설명을 진행하겠다. Client는 우선 생성 시 이를 식별할 ClientID
가 부여된다. - 형태로 부여되고 Sequence의 경우 ClientType
관계없이 전역적으로 증가한다. 따라서 만약 지금 IBC Client를 처음 만드는 것이라면 07-tendermint-0이라는 ClientID
가 부여될 것이다. 이 ClientID
를 기반으로 /clients/ 경로를 가지는 ClientStore
를 생성한다. 이후 해당 Store에 ClientState
, ConsensusState
, ConsensusMetadata
를 저장한다.
ClientState
에 저장되는 정보로는 체인을 식별할ChainID
, Relayer에 의해 업데이트 되는 상태의 유효성을 검증하기 위한TrustLevel
, 상대 체인의UnbondingPeriod
, 상태 업데이트를 위한 헤더 제출시간에 제약을 거는TrustingPeriod
,MaxClockDrift
, 최근 클라이언트 상태를 업데이트한 높이인LatestHeight
, 잘못된 상태 업데이트로 클라이언트가 Frozen 될 때 저장되는 높이인FrozenHeight
, 상태 검증을 위한[ProofSpecs](https://github.com/cosmos/ibc-go/blob/2551dea41cd3c512845007ca895c8402afa9b79f/modules/core/23-commitment/types/merkle.go#L18)
, Client 업그레이드를 위한UpgradePath
를 포함한다.ConsensusState
의 경우LatestHeight
에 대응하는Timestamp
(BlockTime)와 이 때의Root
(Tendermint에서는 app hash), 다음 높이의 Validator set의 해시값인NextValidatorHash
를 포함한다.ConsensusMetadata
의 경우 Client에서 Create/Update/Upgrade가 진행될 때LatestHeight
를 키로 하여BlockHeight
,BlockTime
을 저장한다.
마지막으로는 ClientState
가 Active 상태인지 확인하는데 1)LatestHeight
에 해당하는 ConsensusState
가 존재하지 않거나 2) 현재 블록의 header.BlockTime
이 [ConsensusState.Timestamp
+ TrustingPeriod
]를 초과한 경우 해당 Client는 Expired 상태로 간주된다. 이 모든 과정을 통과하면 상대 체인에 대한 IBC Client가 생성된다. 또한, 연결될 체인에서도 해당 체인에 대해 위 과정을 동일하게 거쳐 IBC Client 생성이 필요하다.
Client가 생성된 이후에는 이 Client가 Expired되지 않게 UpdateClient
메시지를 통해 주기적으로 업데이트가 필요하다. 누구나 가능하지만 대부분 Relayer가 진행한다. 해당 메시지를 체인으로 보내면 우선 Client 상태가 Active인지 확인하고 ClientMessage
검증을 진행한다. ClientMessage
의 종류로는 일반적인 업데이트를 할 때 전송하는 Header
와 동일한 높이에서 서로 충돌하는 Header
를 발견했을 때 이를 증거로 제출해서 Client를 Frozen 상태로 만드는 Misbehaviour
가 있다. Header의 경우 아래 구조체를 가지고 아래 과정을 통해 생성된다.
type Header struct {
*types2.SignedHeader
ValidatorSet *types2.ValidatorSet
TrustedHeight types.Height
TrustedValidators *types2.ValidatorSet
}
type Misbehaviour struct {
Header1 *Header
Header2 *Header
}
type Header struct {
// basic block info
Version version.Consensus
ChainID string
Height int64
Time time.Time
// prev block info
LastBlockId BlockID
LastCommitHash []byte
DataHash []byte
// hashes from the app output from the prev block
ValidatorsHash []byte
NextValidatorsHash []byte
ConsensusHash []byte
AppHash []byte
LastResultsHash []byte
// consensus info
EvidenceHash []byte
ProposerAddress []byte
}
type ValidatorSet struct {
Validators []*Validator
Proposer *Validator
TotalVotingPower int64
}
내가 신뢰할 블록 높이인 TrustedHeight
를 설정하고 TrustedHeight+1
에서의 ValidatorSet
을 Staking 모듈의 HistoricalInfo
를 통해 추출하고 TrustedValidators
로 설정한다. Header
의 ValidatorSet
은 내가 업데이트할 상대 체인 특정 높이에서의 ValidatorSet
을 의미하고 SignedHeader
는 그 때 Tendermint에서 서명된 Header
를 의미한다. Header
를 제출한 이후의 과정을 보면 다음과 같다. TrustedHeight
에 매칭되는 ConsensusState
를 불러온 후 여기에 저장된 NextValidatorHash
와 TrustedValidators
의 hash가 동일한지 비교한다. 이후 TrustedHeight
와 ConsensusState
의 Time
, NextValidatorHash
로 도출된 trustedHeader
를 생성한다. 신뢰할 수 있는 데이터인 trustedHeader
와 TrustedValidators
데이터를 기반으로 아직 신뢰할 수 없는 SignedHeader
, ValidatorSet
데이터의 검증을 Tendermint 내부의 light.Verify
를 통해 진행하고 통과하면 IBC Client의 ClientState
, ConsensusState
가 업데이트 된다.
이 때 trustedHeader
가 이번에 제출된 Header
의 바로 인접블록 헤더인 경우와 아닌 경우 검증방법이 약간 달라진다. 먼저 인접블록 헤더인 경우 현재 BlockTime이 trustedHeader
의 Timestamp
와 ClientState
의 TrustingPeriod
를 더한 값보다 작은지 검사한다. 이후 새로 제출된 헤더의 Timestamp
나 BlockHeight
가 trustedHeader
의 값보다 큰지 검증 후 제출된 헤더의 Timestamp
가 (ctx.BlockTime
+MaxClockDrift
) 값보다 큰지 검증한다. 이후 제출된 헤더의 ValidatorHash
가 ValidatorSet
의 hash 값과 동일한 지, trustedHeader
의 NextvalidatorHash
가 이 hash 값과 동일한 지 검증을 진행한다. 마지막으로는 제출된 헤더에 서명한 votingPower
의 총합이 제출한 ValidatorSet
의 전체 votingPower
의 2/3를 넘는지 검증한다.
인접블록 헤더가 아닌 경우 인접블록 헤더 검증방법과 거의 동일하지만 연속된 높이의 헤더가 아닌데서 오는 상태의 비연속성을 보정할 추가적인 검증이 필요하고 이 때 사용되는 것이 TrustedValidators
와 Client 생성 시 설정한 TrustLevel
이다. 현재 제출된 헤더에 서명한 Validator 중 TrustedValidators
에 속한 Validator들의 votingPower
합이 TrustedValidators
의 총 votingPower
에 TrustLevel
을 곱한 값보다 큰 경우 이를 유효하게 서명된 헤더로 간주한다. 이 때 TrustLevel
의 기본값은 1/3이고 보안과 운영 관점에서 Client 생성 시 적절하게 조절할 수 있다. Misbehaviour
의 경우 제출된 충돌되는 2개의 헤더가 비인접블록 헤더 검증방법을 통과하는 유효한 헤더인지 검사하고 아닌 경우 거부한다.
이렇게 ClientMessage
를 검증하는 과정을 통과하면 Header
의 경우 제출된 높이에 대한 ConsensusState
가 이미 Client 내에 존재하는지 확인하고 존재한다면 그 값이 같은지 확인한다. 또한, 제출된 높이 앞뒤로 이미 저장된 ConsensusState
가 있는지 확인 후 timestamp 순서가 적절한 지 확인한다. 만약 하나라도 만족하지 못한다면 잘못된 Header를 제출했다고 판명되고 해당 Client는 Frozen 상태가 된다. Misbehaviour
의 경우 제출된 두개 헤더의 BlockHeight가 같은 경우 각 commit hash를 비교해서 다르거나 더 높은 BlockHeight인 Header1의 BlockTime
이 Header2의 BlockTime
보다 낮은 경우 올바른 제출이라 판단해 역시 해당 Client는 Frozen 상태가 된다.
위 과정들을 전부 통과하면 마지막으로 과거에 저장된 ConsensusState
중 timestamp
+TrustPeriod
값이 현 BlockTime을 넘긴 경우 모두 제거해주고 제출된 헤더의 값으로부터 ConsensusState
및 ConsensusMetadata
를 저장한다. 또한, 제출된 헤더의 BlockHeight
가 ClientState
의 LatestHeight
보다 높은 경우 이 값을 갱신 후 저장한다.
불가피하게 상대 체인의 ChainID
나 UnbondingPeriod
, ProofSpecs
, UpgradePath
, LatestHeight
가 변경될 수 있는 상황이 발생할 수 있다(e.g. Hard fork). 이 경우 MsgIBCSoftwareUpgrade를 통해 이미 생성된 IBC Client에 해당 내용을 반영할 수 있다. 우선 체인 A에서 체인 B에 대한 IBC Client를 만든 상태라고 가정하겠다. 체인 B에서 위에 해당하는 파라미터들 중 일부를 변경했을 때 체인 B에서 MsgIBCSoftwareUpgrade를 통해 변경이 반영된 ClientState
에 대한 commitment를 특정 높이에서 생성하도록 x/upgrade 모듈을 활용해 설정한다. 이후 체인 B에서 해당 업그레이드가 실행되면 아래 abci_query를 통해 이 업그레이드에 대한 commitment를 가져온다.
RequestQuery{
Path: "store/upgrade/key",
Height: upgradedHeight,
Data: "upgradedIBCState//upgradedClient",
Prove: true
}
RequestQuery{
Path: "store/upgrade/key",
Height: upgradedHeight,
Data: "upgradedIBCState//upgradedConsState",
Prove: true
}
이후 변경된 ClientState
, ConsensusState
와 이에 대한 증명을 첨부해 체인 A에 제출하면 merkle proof 검증 이후 해당 ClientID
에 반영된다. 유의할 점은 UpgradeClient는 체인 변경과 관련된 파라미터의 변경만 지원한다는 것이다. 이는 악의적인 사용자가 임의로 IBC Client 속성을 변경해서 패킷 검증을 방해하는 것을 막기 위함이다. 따라서 TrustingPeriod
, TrustLevel
, MaxClockDrift
는 생성 이후 변경이 불가능하고 변경을 원할 경우 새로운 클라이언트를 생성해야 한다.
앞선 설명에서 생성된 IBC Client를 정해진 시간 내에 주기적으로 업데이트 하지 않거나 잘못된 헤더를 제출하는 경우 해당 클라이언트가 Expired나 Frozen 상태로 변경됨을 언급했다. 이 경우 더이상 해당 Client를 업데이트 하지 못하고 사용하지 못하게 된다. 이를 다시 Active 상태로 만들기 위해서는 새로운 IBC Client를 만들어서 이것의 상태를 기존 정지된 IBC Client로 대체하는 과정이 필요하다. 이 때 새로운 IBC Client는 기존 IBC Client와 TrustLevel
, UnbondingPeriod
, MaxClockDrift
, ProofSpecs
, UpgradePath
가 동일해야 복구를 위한 유효한 클라이언트라고 간주된다. 이후 MsgRecoverClient를 거버넌스 프로포절을 통해 실행시키면 해당 IBC Client는 복구된다.
Connection
앞에선 IBC 패킷 전송 시 검증을 위해 상대방 체인의 상태를 지속적으로 업데이트하고 유지하는 Client에 대해 다뤄보았다. 이번 장에서는 Connection에 대해서 알아볼 것이다. Connection은 패킷처리 방법[IBC 호환 버전 및 그에 연관된 허용 가능 기능들, 상대 체인 commitment merkle prefix, ClientState
가 업데이트된 후 패킷 검증을 위해 기다려야 하는 시간(a.k.a. DelayPeriod
)]을 상대 체인과 합의하기 위해 설계된 요소다. 해당 내용은 ConnectionEnd
객체에 저장된다.
하나의
ClientID
는 여러ConnectionID
와 연결될 수 있다.
type ConnectionEnd struct {
ClientId string
Versions []*Version
State State
Counterparty Counterparty
DelayPeriod uint64
}
type Counterparty struct {
ClientId string
ConnectionId string
// commitment merkle prefix of the counterparty chain.
Prefix types.MerklePrefix
}
양 체인간 Connection이 생성되는 과정은 다음과 같다. (Chain A : ibc-1, Chain B : ibc-0)
[Chain A side]ConnOpenInit
실행 전 Chain A 위의 Chain B Client에 대해 릴레이어가
UpdateClient
실행한다.msg에 포함된 clientID를 기준으로 clientState를 받아와서 Active 상태인지 확인.
순차증가로
ConnectionID
가 생성되고 (connection/<sequence>
) 이를 clientID에 추가하고 INIT 상태의ConnectionEnd
객체를 생성함. 이 때 호환되는 IBCVersion
들도 같이 설정. IBC v8 기준으로Version
의 기본 값은 Identifier=1, Features=[ORDER_ORDERED, ORDER_UNORDERED]ConnectionEnd
등록
[Chain B side]ConnOpenTry
connection_open_init
이벤트를 캐치한 후 릴레이어가 Chain B 위의 Chain A IBC Client에 대해UpdateClient
실행.Chain A에서 저장하고 있는 Chain B의
ClientState
,LatestHeight
와 Chain A의ClientID
,ConnectionID
,MerklePrefix
,Versions
,DelayPeriod
. 마지막으로는 앞의 내용들이 전부 반영된 높이(proofHeight
)를 기준으로 abci_query를 통해ClientState
,ConsensusState
,ConnectionEnd
데이터 존재에 대한 증명을 받아와MsgConnectionOpenTry
를 전송. 이 때 각 증명의Path
는 “store//key” 이고Data
는 각각 “clients//clientState”, “clients//consensusStates/”, **“connections/”**이다.Chain A가 저장하고 있는 chain B의 height가 Chain B의 실제 height보다 작은지 확인
Chain A가 저장하고 있는 Chain B의
ClientState
가 실제 Chain B의 정보와 맞는지 확인. 이후LatestHeight
를 기준으로 chain A에 저장되있다고 예상되는 chain B의expectedConsensusState
를 생성Chain A에서 만들었으리라고 예상되는
expectedConnectionEnd
를 생성.Chain B의
connectionID
및 TRYOPEN인connectionEnd
를 생성하고 이 때Version
은 chain A와 호환되는 버전 중 가장 최신의 것을 선택하고DelayPeriod
는 Chain A와 동일하게 선택.해당
connectionID
에 연결된ClientID
가 Active인지 확인 후proofInit
를 활용하여 실제로 Chain A에서 예상되는ConnectionEnd
로 열렸는지VerifyMembership
을 통해 검증.Chain A에서 주장하는대로 해당
ClientID
에 올바른ClientState
를 저장했는지proofClient
를 활용하여VerifyMembership
을 통해 검증Chain A에서 주장하는대로 Chain B의
proofHeight
에서의ConsensusState
가 해당ClientID
에 올바르게 저장되어 있는지proofConsensus
를 활용하여VerifyMembership
을 통해 검증검증이 끝난 후
ConnectionEnd
를ClientID
에 저장.
[Chain A side]ConnOpenAck
connection_open_try
이벤트를 캐치한 후 릴레이어가 체인 A 위에서UpdateClient
실행.chain B에서 저장하고 있는 chain A의 정보에 맞춰
MsgConnectionOpenAck
전송. (ConnOpenTry
와 동일한 로직)chain B가 저장하고 있는 chain A의 height가 chain A의 실제 height보다 작은지 확인,
ConnectionID
기준으로ConnOpenInit
에서 저장한ConnectionEnd
가져오고 INIT 상태인지 확인, chain B에서 Connection 열면서 선택한Version
이 Chain A에서 지원하는지 확인ConnOpenTry
의 4, 5, 7, 8, 9를 Chain A 관점에서 진행.Connection 상태를 OPEN으로 변경,
Versions
를 chian B에서 선택한 것으로 통일하고 Chain B의ConnectionID
를Counterparty
에 설정ConnectionEnd
갱신
[Chain B side]ConnOpenConfirm
connection_open_ack
이벤트를 캐치한 후MsgConnectionOpenAck
전송.msg의
ConnectionID
기준으로 TRYOPEN인지 확인.Chain A에서 생성된
expectedConnectionEnd
를VerifyMembership
을 통해 검증 (OPEN)Chain B의
ConnectionEnd
도 OPEN으로 변경 후 갱신
Channel, Port
앞서 설명한 Client와 Connection은 추상화된 IBC 패킷을 믿고 처리하기 위한 제반설정이라면 Channel은 전달되는 패킷들이 어떤 Application 모듈에서 처리되고 이 때 처리 순서는 어떻게 할 것인지에 대한 정보를 저장한다. 즉, 패킷들이 설정된 ChannelID
에 따라 특정 경로로만 흐르도록 데이터 경로를 추상화한 것이다. 각 패킷은 자기가 속하는 Channel에 대하여 Sequence
를 가지고 이는 채널 생성 시 Ordering
설정에 따라 FIFO로 처리할 지 Sequence
를 기준으로 순차처리할지 결정된다. Port는 어떤 Application에서 처리될건지 구체적으로 경로를 지정하는 역할을 하는 인자다. 따라서 아래의 Channel
객체는 항상 **“channelEnds/ports//channels/”**를 key로 하여 저장된다.
서로 다른
ChannelID
가 같은ConnectionID
를 사용하는 것도 가능하다.
type Channel struct {
State State
Ordering Order
Counterparty Counterparty
ConnectionHops []string
Version string
}
type Counterparty struct {
// port on the counterparty chain which owns the other end of the channel.
PortId string
// channel end on the counterparty chain
ChannelId string
}
IBC에서는 특정 Port는 특정 Application 모듈 로직만 처리하고, 특정 Channel은 특정 Application 모듈로부터 발생한 패킷을 처리하도록 강제하기 위해 Capability라는 개념을 도입했다. portKeeper.BindPort
를 통해 특정 port path **“ports/”**에 대한 Capability
를 생성하고 ClaimCapability
를 통해 특정 ModuleName
에 할당한다. 이러면 추후 이 PortID
와 관련 해서 Channel을 열거나 기타 로직(e.g. 핸들러 호출)을 수행할 때 등록한 Module에 대해서만 연계가 된다. 특정 PortID
에 대한 Channel을 열 때를 예시로 들면 PortID
를 기반으로 연결된 IBCModule
을 호출한다. 이후 내부 로직에서 해당 채널 사용권 주장을 위한 Capability
(capabilities/ports//channels/)를 생성하고 이를 아까 호출된 IBCModule
의 Channel 생성 관련 콜백함수에서 ClaimCapability
를 통해 역시 해당 ModuleName
에 할당한다. 이후 channelkeeper.SendPacket
을 통해 IBC 패킷을 전송할 때 AuthenticateCapability
를 통해 검증하기 때문에 적절한 Capability
를 제공하지 못하면 그 동작은 실패하게 된다.
양 체인간 Channel이 생성되는 과정은 다음과 같다. (Chain A : ibc-1, Chain B : ibc-0)
[Chain A side]ChanOpenInit
Chain B가 연결된
ConnectionID
, 채널을 열 Chain A, B의PortID
, 채널 자체의 패킷 처리 규칙(Ordering
),Version
을MsgChanOpenInit
에 포함시켜 Chain A로 전송msg에 포함된
PortID
랑 매칭되는 module 이름과 이에 연결된Capability
, 콜백 라우터를 불러옴.msg에 포함된
ConnectionID
에 연결된 Client가 Active인지 확인.불러온
portCap
이 실제PortID
랑 매칭되는지 확인.ChannelID
생성.ConnectionID
와 마찬가지로 “channel-” 형태로 순차증가한다. 이후ChannelCapabilityPath
에 대한Capability
생성아까 호출된 콜백 라우터의
OnChanOpenInit
을 실행. 이 과정에서ChannelCapabilityPath
의Capability
가 해당 Application 모듈에 할당된다.Channel
등록 (INIT)
[Chain B side]ChanOpenTry
channel_open_init
이벤트를 캐치한 릴레이어가 여기 포함된srcPortID
,srcChannelID
,Channel
값을 기반으로 체인 B로ChanOpenTry
메시지를 보냄. 이 때 Chain A에서 채널이 제대로 열렸다는 것을 검증하기 위해 connection handshake 때처럼proofInit
을 첨부. 이 때의 abci_query ->
Path
: **store/<24-host_StoreKey>/key
**Data
: channelEnds/ports//channels/ChanOpenInit
의 2와 동일ChanOpenInit
의 4와 동일msg에 포함된
ConnectionID
가 OPEN인지 확인.주어진 데이터를 기반으로 Chain A에서 생성됐다고 예상되는
expectedChannel
을 만들어서proofInit
,proofHeight
와 함께 검증.ChanOpenInit
의 5와 동일아까 호출된 콜백 라우터의
OnChanOpenTry
를 실행. 이 과정에서ChannelCapabilityPath
의Capability
가 해당 Application 모듈에 할당된다.Channel
등록 (TRYOPEN)
[Chain A side]ChanOpenAck
channel_open_try
이벤트를 캐치한 릴레이어가srtPortID
,srcChannelID
,dstPortID
,dstChannelID
,Version
,ConnectionID
,proofTry
기반으로 Chain A로ChanOpenAck
메시지를 보냄msg에 포함된
srcChannelID
,srcPortID
랑 매칭되는 module 이름과 이에 연결된Capability
, 콜백 라우터를 불러옴불러온
Capability
가 실제 (ChannelID
,PortID
)랑 매칭되는지 확인srcChannelID
에 연결된ConnectionID
가 OPEN인지 확인주어진 데이터를 기반으로 Chain B에서 생성됐다고 예상되는
expectedChannel
을 만들어서proofTry
,proofHeight
와 함께 검증.Channel 정보 갱신 (OPEN).
아까 호출된 콜백 라우터의
onChanOpenAck
를 실행.
[Chain B side]ChanOpenConfirm
channel_open_ack
이벤트를 캐치한 릴레이어가dstPortID
,dstChannelID
,proofAck
을ChanOpenConfirm
메시지에 포함해 Chain B로 보냄.ChanOpenAck
의 2, 3, 4, 5를 Chain B 관점에서 실행Channel 정보 갱신 (OPEN)
아까 호출된 콜백 라우터의
onChanOpenConfirm
을 실행.
Channel의 경우 열었던 Channel을 다시 닫을 수 있다. 이 경우는 열 때처럼 4-handshake없이 한쪽에서 바로 닫고(CloseInit
) 이에 대한 proofInit
을 상대 체인에 넘겨서 검증하면 해당 Channel은 비활성화된다.
이번 글에서는 IBC의 내부 동작에 해당하는 Client, Connection, Channel, Port와 IBC 패킷 증명을 위한 Vector Commintment
에 대해 알아보았다. 다음 글에서는 실제 위에처럼 IBC 설정이 체인간 이뤄진 이후 실제 IBC 패킷이 처리되는 과정과 현재 구현되어 있는 IBC Application, Middleware에 대해 다뤄보겠다.
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