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

Jaeseung LeeJaeseung Lee
14 min read

들어가며

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 메시지를 내 체인에 보내야 한다. 해당 메시지는 ClientStateConsensusState를 포함하고 있다.

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을 저장한다.

마지막으로는 ClientStateActive 상태인지 확인하는데 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로 설정한다. HeaderValidatorSet은 내가 업데이트할 상대 체인 특정 높이에서의 ValidatorSet을 의미하고 SignedHeader는 그 때 Tendermint에서 서명된 Header를 의미한다. Header를 제출한 이후의 과정을 보면 다음과 같다. TrustedHeight에 매칭되는 ConsensusState를 불러온 후 여기에 저장된 NextValidatorHashTrustedValidators의 hash가 동일한지 비교한다. 이후 TrustedHeightConsensusStateTime, NextValidatorHash로 도출된 trustedHeader를 생성한다. 신뢰할 수 있는 데이터인 trustedHeaderTrustedValidators데이터를 기반으로 아직 신뢰할 수 없는 SignedHeader, ValidatorSet 데이터의 검증을 Tendermint 내부의 light.Verify를 통해 진행하고 통과하면 IBC Client의 ClientState, ConsensusState가 업데이트 된다.

이 때 trustedHeader가 이번에 제출된 Header의 바로 인접블록 헤더인 경우와 아닌 경우 검증방법이 약간 달라진다. 먼저 인접블록 헤더인 경우 현재 BlockTime이 trustedHeaderTimestampClientStateTrustingPeriod를 더한 값보다 작은지 검사한다. 이후 새로 제출된 헤더의 TimestampBlockHeighttrustedHeader의 값보다 큰지 검증 후 제출된 헤더의 Timestamp가 (ctx.BlockTime+MaxClockDrift) 값보다 큰지 검증한다. 이후 제출된 헤더의 ValidatorHashValidatorSet의 hash 값과 동일한 지, trustedHeaderNextvalidatorHash가 이 hash 값과 동일한 지 검증을 진행한다. 마지막으로는 제출된 헤더에 서명한 votingPower의 총합이 제출한 ValidatorSet의 전체 votingPower의 2/3를 넘는지 검증한다.

인접블록 헤더가 아닌 경우 인접블록 헤더 검증방법과 거의 동일하지만 연속된 높이의 헤더가 아닌데서 오는 상태의 비연속성을 보정할 추가적인 검증이 필요하고 이 때 사용되는 것이 TrustedValidators와 Client 생성 시 설정한 TrustLevel이다. 현재 제출된 헤더에 서명한 Validator 중 TrustedValidators에 속한 Validator들의 votingPower 합이 TrustedValidators의 총 votingPowerTrustLevel을 곱한 값보다 큰 경우 이를 유효하게 서명된 헤더로 간주한다. 이 때 TrustLevel의 기본값은 1/3이고 보안과 운영 관점에서 Client 생성 시 적절하게 조절할 수 있다. Misbehaviour의 경우 제출된 충돌되는 2개의 헤더가 비인접블록 헤더 검증방법을 통과하는 유효한 헤더인지 검사하고 아닌 경우 거부한다.

이렇게 ClientMessage를 검증하는 과정을 통과하면 Header의 경우 제출된 높이에 대한 ConsensusState가 이미 Client 내에 존재하는지 확인하고 존재한다면 그 값이 같은지 확인한다. 또한, 제출된 높이 앞뒤로 이미 저장된 ConsensusState가 있는지 확인 후 timestamp 순서가 적절한 지 확인한다. 만약 하나라도 만족하지 못한다면 잘못된 Header를 제출했다고 판명되고 해당 Client는 Frozen 상태가 된다. Misbehaviour의 경우 제출된 두개 헤더의 BlockHeight가 같은 경우 각 commit hash를 비교해서 다르거나 더 높은 BlockHeight인 Header1BlockTimeHeader2BlockTime보다 낮은 경우 올바른 제출이라 판단해 역시 해당 Client는 Frozen 상태가 된다.

위 과정들을 전부 통과하면 마지막으로 과거에 저장된 ConsensusStatetimestamp+TrustPeriod 값이 현 BlockTime을 넘긴 경우 모두 제거해주고 제출된 헤더의 값으로부터 ConsensusStateConsensusMetadata를 저장한다. 또한, 제출된 헤더의 BlockHeightClientStateLatestHeight보다 높은 경우 이 값을 갱신 후 저장한다.

불가피하게 상대 체인의 ChainIDUnbondingPeriod, 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를 정해진 시간 내에 주기적으로 업데이트 하지 않거나 잘못된 헤더를 제출하는 경우 해당 클라이언트가 ExpiredFrozen 상태로 변경됨을 언급했다. 이 경우 더이상 해당 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

  1. 실행 전 Chain A 위의 Chain B Client에 대해 릴레이어가 UpdateClient 실행한다.

  2. msg에 포함된 clientID를 기준으로 clientState를 받아와서 Active 상태인지 확인.

  3. 순차증가로 ConnectionID가 생성되고 (connection/<sequence>) 이를 clientID에 추가하고 INIT 상태의 ConnectionEnd 객체를 생성함. 이 때 호환되는 IBC Version들도 같이 설정. IBC v8 기준으로 Version의 기본 값은 Identifier=1, Features=[ORDER_ORDERED, ORDER_UNORDERED]

  4. ConnectionEnd등록

[Chain B side]ConnOpenTry

  1. connection_open_init 이벤트를 캐치한 후 릴레이어가 Chain B 위의 Chain A IBC Client에 대해 UpdateClient 실행.

  2. 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/”**이다.

  3. Chain A가 저장하고 있는 chain B의 height가 Chain B의 실제 height보다 작은지 확인

  4. Chain A가 저장하고 있는 Chain B의 ClientState가 실제 Chain B의 정보와 맞는지 확인. 이후 LatestHeight를 기준으로 chain A에 저장되있다고 예상되는 chain B의 expectedConsensusState를 생성

  5. Chain A에서 만들었으리라고 예상되는 expectedConnectionEnd를 생성.

  6. Chain B의 connectionIDTRYOPENconnectionEnd를 생성하고 이 때 Version은 chain A와 호환되는 버전 중 가장 최신의 것을 선택하고 DelayPeriod는 Chain A와 동일하게 선택.

  7. 해당 connectionID에 연결된 ClientIDActive인지 확인 후 proofInit를 활용하여 실제로 Chain A에서 예상되는 ConnectionEnd로 열렸는지 VerifyMembership을 통해 검증.

  8. Chain A에서 주장하는대로 해당 ClientID에 올바른 ClientState를 저장했는지 proofClient를 활용하여 VerifyMembership을 통해 검증

  9. Chain A에서 주장하는대로 Chain B의 proofHeight에서의 ConsensusState가 해당 ClientID에 올바르게 저장되어 있는지 proofConsensus를 활용하여 VerifyMembership을 통해 검증

  10. 검증이 끝난 후 ConnectionEndClientID저장.

[Chain A side]ConnOpenAck

  1. connection_open_try 이벤트를 캐치한 후 릴레이어가 체인 A 위에서 UpdateClient 실행.

  2. chain B에서 저장하고 있는 chain A의 정보에 맞춰 MsgConnectionOpenAck 전송. (ConnOpenTry와 동일한 로직)

  3. chain B가 저장하고 있는 chain A의 height가 chain A의 실제 height보다 작은지 확인, ConnectionID 기준으로 ConnOpenInit 에서 저장한 ConnectionEnd 가져오고 INIT 상태인지 확인, chain B에서 Connection 열면서 선택한 Version이 Chain A에서 지원하는지 확인

  4. ConnOpenTry의 4, 5, 7, 8, 9를 Chain A 관점에서 진행.

  5. Connection 상태를 OPEN으로 변경, Versions를 chian B에서 선택한 것으로 통일하고 Chain B의 ConnectionIDCounterparty에 설정

  6. ConnectionEnd갱신

[Chain B side]ConnOpenConfirm

  1. connection_open_ack 이벤트를 캐치한 후 MsgConnectionOpenAck 전송.

  2. msg의 ConnectionID 기준으로 TRYOPEN인지 확인.

  3. Chain A에서 생성된 expectedConnectionEndVerifyMembership을 통해 검증 (OPEN)

  4. Chain B의 ConnectionEndOPEN으로 변경 후 갱신

Channel, Port

앞서 설명한 ClientConnection은 추상화된 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/)를 생성하고 이를 아까 호출된 IBCModuleChannel 생성 관련 콜백함수에서 ClaimCapability를 통해 역시 해당 ModuleName에 할당한다. 이후 channelkeeper.SendPacket을 통해 IBC 패킷을 전송할 때 AuthenticateCapability를 통해 검증하기 때문에 적절한 Capability를 제공하지 못하면 그 동작은 실패하게 된다.

양 체인간 Channel이 생성되는 과정은 다음과 같다. (Chain A : ibc-1, Chain B : ibc-0)

[Chain A side]ChanOpenInit

  1. Chain B가 연결된 ConnectionID, 채널을 열 Chain A, B의 PortID, 채널 자체의 패킷 처리 규칙(Ordering), VersionMsgChanOpenInit 에 포함시켜 Chain A로 전송

  2. msg에 포함된 PortID랑 매칭되는 module 이름과 이에 연결된 Capability, 콜백 라우터를 불러옴.

  3. msg에 포함된 ConnectionID에 연결된 Client가 Active인지 확인.

  4. 불러온 portCap이 실제 PortID랑 매칭되는지 확인.

  5. ChannelID 생성. ConnectionID와 마찬가지로 “channel-” 형태로 순차증가한다. 이후 ChannelCapabilityPath에 대한 Capability 생성

  6. 아까 호출된 콜백 라우터의 OnChanOpenInit을 실행. 이 과정에서 ChannelCapabilityPathCapability가 해당 Application 모듈에 할당된다.

  7. Channel등록 (INIT)

[Chain B side]ChanOpenTry

  1. 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/

  2. ChanOpenInit의 2와 동일

  3. ChanOpenInit의 4와 동일

  4. msg에 포함된 ConnectionIDOPEN인지 확인.

  5. 주어진 데이터를 기반으로 Chain A에서 생성됐다고 예상되는 expectedChannel을 만들어서 proofInit, proofHeight와 함께 검증.

  6. ChanOpenInit의 5와 동일

  7. 아까 호출된 콜백 라우터의 OnChanOpenTry를 실행. 이 과정에서 ChannelCapabilityPathCapability가 해당 Application 모듈에 할당된다.

  8. Channel등록 (TRYOPEN)

[Chain A side]ChanOpenAck

  1. channel_open_try 이벤트를 캐치한 릴레이어가 srtPortID, srcChannelID, dstPortID, dstChannelID, Version, ConnectionID, proofTry기반으로 Chain A로 ChanOpenAck 메시지를 보냄

  2. msg에 포함된 srcChannelID, srcPortID랑 매칭되는 module 이름과 이에 연결된 Capability, 콜백 라우터를 불러옴

  3. 불러온 Capability가 실제 (ChannelID, PortID)랑 매칭되는지 확인

  4. srcChannelID에 연결된 ConnectionIDOPEN인지 확인

  5. 주어진 데이터를 기반으로 Chain B에서 생성됐다고 예상되는 expectedChannel을 만들어서 proofTry, proofHeight와 함께 검증.

  6. Channel 정보 갱신 (OPEN).

  7. 아까 호출된 콜백 라우터의 onChanOpenAck를 실행.

[Chain B side]ChanOpenConfirm

  1. channel_open_ack 이벤트를 캐치한 릴레이어가 dstPortID, dstChannelID, proofAckChanOpenConfirm 메시지에 포함해 Chain B로 보냄.

  2. ChanOpenAck의 2, 3, 4, 5를 Chain B 관점에서 실행

  3. Channel 정보 갱신 (OPEN)

  4. 아까 호출된 콜백 라우터의 onChanOpenConfirm을 실행.

Channel의 경우 열었던 Channel을 다시 닫을 수 있다. 이 경우는 열 때처럼 4-handshake없이 한쪽에서 바로 닫고(CloseInit) 이에 대한 proofInit을 상대 체인에 넘겨서 검증하면 해당 Channel은 비활성화된다.

이번 글에서는 IBC의 내부 동작에 해당하는 Client, Connection, Channel, Port와 IBC 패킷 증명을 위한 Vector Commintment에 대해 알아보았다. 다음 글에서는 실제 위에처럼 IBC 설정이 체인간 이뤄진 이후 실제 IBC 패킷이 처리되는 과정과 현재 구현되어 있는 IBC Application, Middleware에 대해 다뤄보겠다.

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