[IBC Research] 2. IBC Handshake & 트랜잭션 검증

Pangyoalto
Decipher Media |디사이퍼 미디어
17 min readJul 24, 2022

본 게시글은 Cosmos의 IBC(Inter-Blockchain Communication protocol)에 대해 분석합니다. IBC 분석 글은 시리즈로 게시되었으며, 본 글을 읽기 전 IBC의 개요를 다룬 1편을 먼저 읽으시는 것을 추천드립니다. 이번 글에서는 IBC Handshake 및 상대 체인에서 받은 트랜잭션 검증에 대해 다룹니다.

IBC (Source: github)

Author
신우진(@pangyoalto)
Seoul Nat’l Univ. Blockchain Academy Decipher(@decipher-media)
Reviewed By 정재환

[IBC Research]

  1. Cosmos IBC Process
  2. IBC Handshake & 트랜잭션 검증
  3. ICA & IBC query

목차

  1. Intro
  2. Light client, Relayer
  3. Handshake
  4. IAVL 트리
  5. 트랜잭션 검증(Commitment Proof)
  6. 마무리

해당 게시글이 사용한 모든 자료 및 코드는 22.07.24 기준입니다.

  1. Intro

Cosmos 생태계 외 체인 대부분은 체인 간 통신할 때 브릿지 프로토콜을 사용합니다. 보안 측면에서 IBC와 타 브릿지 프로토콜의 가장 큰 차이점은 무엇일까요? 브릿지 프로토콜은 써드 파티(3rd parties)를 믿어야 하는 반면 IBC는 연결하는 상대 체인을 믿는 것입니다. 브릿지 프로토콜은 검증 과정을 내부에서 수행을 하므로 통신하는 체인은 브릿지 프로토콜을 믿어야 하는 문제가 생깁니다. 즉, 브릿지 프로토콜이 잘못된 행동을 한다면 체인에서 알아차릴 수 없습니다. 반면, IBC에서는 검증 과정을 패킷을 전달해주는 릴레이어(Relayer)가 하지 않고 패킷을 받는 체인에서 수행하게 됩니다. 그래서 IBC를 하는 체인은 릴레이어가 잘못된 행동을 하더라도 해당 내용을 무시할 수 있습니다.

이를 위해서는 패킷을 받는 체인은 1) 상대 체인이 위장한 공격자의 체인이 아닌 진짜 상대 체인인지 알아야 하고, 2) 상대 체인이 패킷으로 보낸 트랜잭션이 정말 상대 체인에서 발생한 트랜잭션인지 알아야 합니다.

이번 글에서는 1)을 확인하는 IBC Handshake 과정과 2)을 확인하는 Commitment Proof에 대해 알아볼 것입니다. IBC HandshakeCommitment Proof를 이해하려면 라이트 클라이언트릴레이어 같은 IBC의 구성 요소부터 IAVL 트리와 같은 자료구조까지 알아야 할 지식이 있습니다.

따라서 이번 글에서는 검증 과정인 Handshake와 Commitment Proof를 바로 설명하지 않고, 배경지식을 먼저 익히고 검증 과정을 설명하는 순서로 진행하겠습니다.

2. Light client, Relayer

패킷을 받는 체인이 스스로 검증하려면 패킷의 내용과 상대 체인의 상태를 대조를 할 수 있어야 합니다. 따라서 통신할 체인의 상태를 지속해서 가지고 있을 필요가 있습니다. 이 역할을 하는 것이 바로 라이트 클라이언트입니다.

라이트 클라이언트(Light client)는 체인 내부에 있는 객체로, 상대 체인의 상태 일부를 트래킹합니다. 라이트 클라이언트는 풀노드와 같은 물리적인 하드웨어가 아닙니다. 각 체인의 노드는 라이트 클라이언트를 내부에 논리적인 객체로 가지고 있고, 곧 설명할 릴레이어로부터 상대 체인의 상태를 계속 업데이트 받습니다.

코드를 보면 라이트 클라이언트가 무슨 역할을 하는지 좀 더 쉽게 이해할 수 있습니다. 라이트 클라이언트는 클라이언트 스테이트컨센서스 스테이트를 가지고 있습니다.

ibc-go/modules/light-clients/07-tendermint/types/tendermint.pb.go

클라이언트 스테이트는 상대 체인의 클라이언트(노드)의 상태를 나타냅니다. ChainID와 같은 상대 체인을 구별할 수 있는 필드부터 LatestHeight 같이 최근 블록 정보를 나타내는 필드까지 있습니다. TrustingPeriod는 현재 라이트 클라이언트가 가지고 있는 상대 체인의 블록 헤더를 신뢰할 수 있는 기간을 말합니다. 만약 라이트 클라이언트가 TrustingPeriod 이내로 업데이트가 되지 않는다면, 라이트 클라이언트는 기능이 만료됩니다.

ibc-go/modules/light-clients/07-tendermint/types/tendermint.pb.go

컨센서스 스테이트는 상대 체인의 컨센서스 알고리즘에 의한 상태 업데이트를 검증할 수 있게 해줍니다. 클라이언트 스테이트에 비해 트래킹하는 필드가 적은데, 가장 중요한 필드는 Root입니다. Root는 상대 체인의 어플리케이션 스테이트의 해시입니다. IBC를 통해 패킷과 함께 넘어온 머클 증명을 검증하는 데 사용이 됩니다.

라이트 클라이언트는 클라이언트 스테이트와 컨센서스 스테이트를 유지하며 풀 노드와 달리 모든 상태 및 트랜잭션, 블록을 트래킹하지 않고 Timestamp, Root 해시와 같은 검증에 꼭 필요한 정보들만 트래킹합니다.

그렇다면 라이트 클라이언트의 상태를 업데이트해 주고, 체인 간 패킷을 전달해주는 주체는 무엇일까요? IBC에서는 상대 체인이 직접 메시지를 보내지 않습니다. 대신 릴레이어(Relayer)라는 오프체인 프로세스가 체인의 상태를 읽고 트랜잭션을 패킷의 형태로 제출해줍니다.

릴레이어는 IBC 통신을 할 양 체인의 풀 노드에 접근할 수 있습니다. 정확히는 풀 노드의 텐더민트 엔진의 웹 소켓 및 RPC 엔드포인트로 이벤트를 구독 및 머클 증명을 쿼리합니다.

릴레이어가 텐더민트 엔진에 접근을 할 수 있으니 라이트 클라이언트에게 상대 체인의 정보를 업데이트해줄 수 있습니다. IBC 트랜잭션으로 발생하는 패킷은 다음과 같은 과정을 통해 전달됩니다.

  1. A 체인의 모듈이 B 체인으로 트랜잭션을 보내고 싶을 때, A 체인에서 실행된 IBC 트랜잭션은 패킷 정보를 이벤트 형태로 방출(emit)하고(텐더민트의 RDBMS에 저장이 됩니다), Commitment Proof를 온체인에 저장합니다.
  2. 릴레이어는 이벤트를 패킷으로 만들고 Commitment Proof와 함께 B 체인의 라이트 클라이언트로 보냅니다.
  3. 라이트 클라이언트는 Commitment Proof를 검증합니다.
  4. 검증이 완료되면 패킷 데이터에 해당하는 행동(트랜잭션)을 수행합니다.

릴레이어는 체인 간 IBC 메시지를 제출해야 하기 때문에 가스비를 제출할 fee가 들어있는 주소의 private key가 필요합니다. 비용이 들어가지만, 메시지를 전달함으로써 온체인 상에서 인센티브를 받는 구조를 설계한 체인이 아직 존재하지 않습니다.

IBC의 검증 과정에서 릴레이어의 역할이 상당히 중요합니다. 브릿지처럼 검증 과정까지 관여하지 않지만, 라이트 클라이언트의 상태를 업데이트하고 패킷을 전달해주는 역할은 IBC에 필수입니다. ICS(interchain standard) spec으로 릴레이어에게 유저나 체인으로부터 인센티브를 제공하는 메커니즘이 제안되었지만, 아직 사용하는 체인은 거의 없습니다. 현재는 체인이 직접 릴레이어 지갑에 funding을 하거나 fee grant를 운영하는 등으로 운영이 되고 있습니다.

Relayer and IBC chain (Source: interchainacademy)

릴레이어는 두 체인 간 여러 개가 존재할 수 있어 일부 릴레이어가 정지하더라도 IBC는 정상적으로 작동할 수 있습니다.

릴레이어 내부에서는 어떠한 패킷에 대해 검증을 하지 않고 단지 전달만 하기 때문에 릴레이어를 신뢰할 필요가 없습니다. 릴레이어가 잘못된 패킷을 전달하더라도 문제가 없다는 의미입니다. 릴레이어가 체인의 라이트 클라이언트를 생성하기 위한 최초 메시지를 전달한 이후부터 라이트 클라이언트가 릴레이가 보낸 메시지들을 전부 검증할 수 있기 때문입니다.

3. Handshake

IBC Overview (Source: interchainacademy)

블록체인은 새로운 블록을 독립적으로 생성 및 확인합니다. 그래서 블록을 안전하게 검증할 수 있습니다. 반면 다른 체인으로부터 오는 패킷들은 전송이 늦어질 수 있고, 검열될 수도 있고, 순서가 바뀔 수도 있습니다. 이는 올바른 패킷을 받더라도 해석할 때 잘못하여 패킷을 보낸 체인의 의도와 다른 행동을 초래할 수 있습니다. 따라서 IBC 프로토콜은 패킷의 순서가 바뀌지 않고 받는 것과 패킷이 단 한 번만 수행이 되는 것을 보장할 필요가 있습니다.

IBC는 이를 채널(Channel)이라는 추상화(abstraction)를 제공함으로써 해결합니다. 상대 체인으로 패킷을 전달할 때는 채널을 사용해 보내게 됩니다. 채널의 역할은 크게 3가지가 있습니다.

  1. 두 모듈 간 패킷을 전달
  2. 패킷이 단 한 번만 실행이 되는 것 보장
  3. 전달한 패킷이 순서대로 도착하는 것 보장(선택 사항)

채널은 패킷을 전달하는 커넥션(Connection)을 추상화한 것입니다. 채널은 커넥션을 추상화해 패킷을 단순히 전달하는 역할을 넘어 패킷의 순서 보장 및 검증 역할까지 수행합니다.

두 체인 간 채널과 커넥션을 생성할 때 Handshake라는 과정을 거치게 됩니다. Handshake는 Web2에서 서버와 클라이언트 간 TCP/IP 프로토콜에서 수행하는 3 way-Handshake 과정과 굉장히 유사합니다. TCP/IP 프로토콜의 Handshake가 안전하고 정확한 전송을 보장하기 위해 패킷을 전송하기 전 커넥션을 생성하는 것처럼, IBC에서의 Handshake는 상대 체인의 정보를 얻고, 다른 체인으로 가장한 공격자의 체인과의 통신을 방지하고 채널 및 커넥션을 생성합니다.

IBC에서 Handshake는 4 way로, 총 4번의 통신을 체인 간 주고받습니다. 채널과 커넥션의 Handshake의 과정은 거의 비슷하므로 커넥션의 Handshake 과정만 다루겠습니다.

4 way Handshake의 과정은 다음과 같습니다.

  1. OpenInit
  2. OpenTry
  3. OpenAck
  4. OpenConfirm

아래 Handshake 설명은 체인 A가 체인 B에게 커넥션을 연결하는 상황을 가정합니다.

OpenInit(Source: interchainacademy)

OpenInit상대 체인에 커넥션을 생성하고 싶다고 알리는 단계입니다. 릴레이어는 상대 체인에 OpenInit 메시지를 전달함과 함께 UpdateClient 메시지도 전달하여 상대 체인의 라이트 클라이언트를 최신 상태로 업데이트합니다.

OpenInit을 호출한 체인 A는 유일한 커넥션 ID를 만들고, 커넥션 ID에 대응되는 커넥션을 만듭니다.

OpenTry(Source: interchainacademy)

체인 B는 체인 A의 메시지를 받고 검증합니다. 체인 B가 체인 A에 대한 정보를 가지고 있는 라이트 클라이언트가 있으므로 정말 체인 A가 올바르게 OpenInit을 한 것인지 확인할 수 있습니다. 검증이 완료되면, 체인 B는 자신의 정보를 담아 응답하는데, 이를 OpenTry라고 합니다.

체인 B도 자신의 정보를 주기 때문에 양 체인이 서로 메시지를 검증하게 됩니다. 이를 위해 OpenInit과 마찬가지로 릴레이어가 체인 A의 라이트 클라이언트의 상태를 업데이트해 줍니다.

OpenAck(Source: interchainacademy)

체인 B에 응답을 받은 체인 A는 라이트 클라이언트가 체인 B의 최신 상태를 가지게 됩니다. 이어지는 OpenAck 단계에서는 OpenTry에서 제안된 프로토콜 버전을 확정 짓습니다. 만약 지원되지 않는 버전이라면 이 단계에서 커넥션은 끊어집니다.

OpenAck 단계까지 완료가 되면 양 체인은 커넥션 스테이트(ConnectionState), 라이트 클라이언트 스테이트(ClientState), 상대 체인의 컨센스스 스테이트(ConsensusState)까지 검증을 완료한 상태입니다.

OpenConfirm(Source: interchainacademy)

OpenConfirm 단계에서는 체인 B가 각 체인의 정보 확인 및 검증이 끝났음을 확정 짓습니다. 즉, Handshake가 마무리되며 커넥션이 성공적으로 생성됩니다.

앞서 말했듯 채널을 생성할 때도 따로 4 way Handshake가 수행되며, 이런 복잡한 과정을 통해 채널까지 생성이 되면 상대 체인에 트랜잭션의 정보가 담긴 패킷을 안전하게 보낼 수 있게 됩니다.

4. IAVL 트리

Cosmos SDK로 만들어진 앱 체인들은 어플리케이션의 상태를 store로 관리를 합니다. 이 store는 Cosmos SDK에서 IAVL 트리로 구현이 되어 있습니다. IAVL 트리는 Balancing 바이너리 트리인 AVL 트리를 변형해 구현한 트리입니다. IAVL 트리가 AVL 트리와 다른 점은 트리의 스냅샷을 저장할 수 있어 이전 상태들을 계속해서 트래킹할 수 있습니다.

Cosmos SDK에서 IAVL 트리는 노드가 변경이 가능한 mutable tree와 노드가 변경할 수 없는 immutable tree를 가지고 있습니다. 정확히 말하면, IAVL 트리의 주 구현체는 Mutable tree이고, Mutable tree가 Immutable tree를 들고 있습니다.

iavl/mutable_tree.go

어플리케이션의 상태는 immutable tree의 형태로 저장이 됩니다. 트리가 수정된다면 노드를 직접 수정하는 것이 아니라 새로운 노드를 생성한 후 대체합니다. 대체된 노드는 orphans에 따로 저장하여 삭제하지 않습니다.

IAVL 트리를 이용하면 트리에 특정 key-value를 저장하고 있는지, 혹은 저장하고 있지 않은지에 대한 증명을 만들어낼 수 있습니다. IAVL 트리가 머클 증명을 만들어 증명의 루트 해시트리의 루트 해시를 비교함으로써 검증합니다.

이해를 돕기 위해 IAVL 트리 구현 github에 제시된 예시를 통해 설명하겠습니다.

            d
/ \
c e
/ \ / \
b c=3 d=4 e=5
/ \
a=1 b=2

위 IAVL 트리가 key는 a이고 value가 1인 쌍이 존재한다는 증명은 어떻게 만들까요?

                 d
/ \
c hash=d6f56d
/ \
b hash=ec6088
/ \
a,hash(1) hash=92fd030

leaf 노드의 b, c와 inner 노드의 e의 해시값이 주어진다면 나머지 노드들의 해시 값을 차례로 계산해 루트인 d의 해시값을 계산할 수 있을 것입니다. 같은 원리로 여러 key-value의 존재 증명도 특정 노드들의 해시값만 존재한다면 트리의 루트 값을 계산할 수 있습니다. 증명은 이 루트 값을 계산하기 위한 값들로 구성이 되어 있습니다.

만약 올바른 루트 해시 값을 미리 알고 있다면, 증명을 통해 계산한 루트 값과 비교하여 해당 증명이 올바른 것인지 판단할 수 있습니다.

IAVL 트리의 이런 성질을 이용한다면, IBC에서 상대 체인의 모든 상태를 전부 들고 있지 않더라도 특정 트랜잭션으로 인한 상태 변화가 일어났는지 확인할 수 있습니다. 이제 IBC에서 어떻게 상대 체인에서 트랜잭션이 발생했는지 확인할 수 있는지 알아보도록 하겠습니다.

5. 트랜잭션 검증(Commitment Proof)

앞서 라이트 클라이언트를 설명할 때 라이트 클라이언트가 컨센서스 스테이트를 가지고 있다고 말씀드렸습니다. 컨센서스 스테이트는 루트라는 필드를 가지고 있는데, 이것을 CommitmentRoot라고 합니다.

블록이 Commit이 되면 체인의 상태 변화가 일어나고, commitment로 바뀐 상태를 CommitmentState라고 합니다. State는 IAVL 트리로 관리가 되므로, 루트의 해시 값을 계산할 수 있는데 이것이 바로 CommitmentRoot입니다. 라이트 클라이언트가 CommitmentRoot를 가지고 있으므로 상대 체인이 증명을 보내면 증명의 루트 해시 값을 계산해 해당 증명이 올바른지 검증이 가능해집니다.

체인 A가 보낸 IBC 트랜잭션이 체인 B에서 검증되는 과정을 예시로 들어보겠습니다.

먼저 체인 B는 릴레이어로부터 체인 A에서 나온 proof들, path, commitmentBytes를 받습니다. Path는 각 proof들을 검증하기 위해 사용되는 key들의 모음이며, commitmentBytes는 첫번째 proof에 존재해야 하는 value입니다.

체인 B는 proof들을 검증하기 위해 체인 A의 상태를 트래킹하는 라이트 클라이언트로부터 클라이언트 스테이트컨센서스 스테이트를 가져옵니다. 이후 여러 전처리 과정을 한 후 컨센서스 스테이트의 commitmentRoot를 가져와 proof들의 검증을 시작합니다.

proof들의 검증은 다음 과정을 거쳐 진행이 됩니다.

  1. 첫번째 proof에 대응하는 key를 path로부터 가져옵니다.
  2. 첫번째 proof로부터 key를 이용해 value 값을 가져옵니다.
  3. 해당 value가 commitmentBytes와 일치하는지 확인합니다.
  4. 1보다 큰 i번째 proof들에 대해서도 마찬가지 작업을 반복합니다. 다만 비교하는 value 값이 commitmentBytes가 아닌 i-1번째 proof의 루트 해시값입니다.
  5. 마지막 proof의 루트 해시 값은 commitmentRoot와 일치해야 합니다.

위 검증이 완료되면 체인 B는 체인 A가 보낸 패킷을 방출한 트랜잭션이 체인 A에서 성공적으로 수행이 되었다고 확인합니다. 이제 체인 B는 패킷을 해석해 자신의 체인에서 트랜잭션을 수행하게 됩니다.

요약하면, 상대 체인으로부터 패킷을 받을 때 해당 패킷을 이벤트로 방출한 트랜잭션이 영향을 미친 state에 대한 증명을 같이 받습니다. 해당 증명들은 i번째 증명의 루트 해시값이 i+1번째 증명에 value 값으로 담겨 있다는 특징이 있습니다. 패킷을 받은 체인은 패킷과 함께 받은 commitmentBytes가 1번째 proof에 담겨 있는 것을 확인하고, i번째 증명의 루트 해시값이 i+1번째 증명에 담겨 있다는 것도 검증합니다. 마지막으로 라이트 클라이언트가 트래킹하고 있는 상대 체인 스테이트의 루트 해시 값이 마지막 증명의 루트 해시값과 같다는 것을 확인함으로써 상대 체인에서 트랜잭션이 올바르게 일어났다는 것을 확인합니다.

6. 마무리

앞서 1편에서는 IBC 트랜잭션을 풀노드로 전송했을 때 어떻게 상대 체인까지 도달하여 실행되는지 알아봤습니다. 이번 글에서는 좀 더 구체적으로 어떻게 트랜잭션이 검증되어 실행이 될 수 있는지 분석했습니다.

다음 글에서는 ICA(interchain accounts)와 아직 존재하지 않는 IBC query의 필요성에 대해 다뤄보겠습니다.

--

--